diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 4cb27b71aaeb5729d4ce357698e3936979bfbee6..0000000000000000000000000000000000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-name: Bug report
-about: For reproducible bugs
-title: ''
-labels: bug
-assignees: ''
-
----
-
-Please search to check for an existing issue (including closed issues, for which the fix may not have yet been released) before opening a new issue: https://github.com/k9mail/k-9/issues?q=is%3Aissue
-
-If you are unsure whether or not you have found a bug please post to the support forum instead: https://forum.k9mail.app
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Click on '...'
-3. Scroll down to '...'
-4. See error
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**Environment (please complete the following information):**
- - K-9 Mail version: [e.g. 5.718]
- - Android version: [e.g. 10]
- - Device: [e.g. Google Pixel 4]
- - Account type: [e.g. IMAP, POP3, WebDAV/Exchange]
-
-**Additional context**
-Add any other context about the problem here.
-
-**Logs**
-Please take some time to [retrieve logs](https://github.com/k9mail/k-9/wiki/LoggingErrors) and attach them here:
-
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a746ed32f7cd59898e0963aec13b70253747dbc0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,77 @@
+name: Bug report
+description: Let us know about crashes or existing functionality not working like it should.
+labels: [ "bug" ]
+body:
+ - type: markdown
+ attributes:
+ value: Thanks for taking the time to fill out this bug report!
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ options:
+ - label: I have used the search function to see if someone else has already submitted the same bug report.
+ required: true
+ - label: I will describe the problem with as much detail as possible.
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: App version
+ description: What's the "latest version" changes all the time, so we need the exact version number. You can find it inside the app under *Settings → About*.
+ placeholder: x.yyy
+ validations:
+ required: true
+ - type: dropdown
+ id: source
+ attributes:
+ label: Where did you get the app from?
+ multiple: false
+ options:
+ - Google Play
+ - F-Droid
+ - Other
+ - type: input
+ id: android_version
+ attributes:
+ label: Android version
+ description: Please mention if you are using a custom ROM!
+ validations:
+ required: true
+ - type: input
+ id: device
+ attributes:
+ label: Device model
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ placeholder: |
+ 1. Go to '…'
+ 2. Click on '…'
+ 3. Scroll down to '…'
+ 4. etc.
+ validations:
+ required: true
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected behavior
+ description: After following the steps, what did you think K-9 Mail would do?
+ validations:
+ required: true
+ - type: textarea
+ id: actual
+ attributes:
+ label: Actual behavior
+ description: What did K-9 Mail do instead? Screenshots might help. Usually, you can take a screenshot by pressing *Power* + *Volume down* for a few seconds.
+ validations:
+ required: true
+ - type: textarea
+ id: logs
+ attributes:
+ label: Logs
+ description: |
+ Please take some time to [retrieve logs](https://github.com/k9mail/k-9/wiki/LoggingErrors) and attach them here.
+
+ Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 1ce9915ba5b11232604d9f8bfebe49c62dc06fce..1b65bc7bd8b013f1431e844dbb51f6069af1a61f 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,9 +1,5 @@
blank_issues_enabled: false
contact_links:
- - name: Support request
- url: https://forum.k9mail.app/c/support
- about: Ask the community for help
- - name: Question?
- url: https://forum.k9mail.app/c/general
- about: For general questions about the project
-
+ - name: K-9 Mail Forum
+ url: https://forum.k9mail.app/
+ about: Most issues are not bugs. Ask the community for help.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index b616b2eba39f0f8058b0a5ba0eaa34b9bd4516f6..0000000000000000000000000000000000000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for K-9 Mail
-title: ''
-labels: enhancement
-assignees: ''
-
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
-
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000000000000000000000000000000000000..491635265f9fdfac6b036d415b2272a1fecf6cab
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,45 @@
+name: Feature request
+description: Suggest an idea for K-9 Mail
+labels: [ "enhancement" ]
+body:
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ options:
+ - label: I have used the search function to see if someone else has already submitted the same feature request.
+ required: true
+ - label: I will describe the problem with as much detail as possible.
+ required: true
+ - label: This issue only contains a request for one single feature, **not** multiple (related) features.
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: App version
+ description: The app version you are currently using. You can find it inside the app under *Settings → About*.
+ placeholder: x.yyy
+ validations:
+ required: true
+ - type: textarea
+ id: problem
+ attributes:
+ label: Problem you are trying to solve
+ description: Give a brief explanation of the problem you are trying to solve.
+ validations:
+ required: true
+ - type: textarea
+ id: solution
+ attributes:
+ label: Suggested solution
+ description: Describe how you would like the app to help you solve that problem. Try to be as specific as possible.
+ validations:
+ required: true
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots / Drawings / Technical details
+ description: |
+ If your request is about (or includes) changing or extending the user interface (UI), describe what the UI would look like and how the user would interact with it.
+
+ Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ee9ba03c0ff60d1bb6bbf9042bbc451366397f87..ac3fb49212767076c57e9b457e103a7acf8a198c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "registry.gitlab.e.foundation:5000/e/apps/docker-android-apps-cicd:legacy"
+image: "registry.gitlab.e.foundation:5000/e/apps/docker-android-apps-cicd:latest"
stages:
- build
diff --git a/app/autodiscovery/providersxml/build.gradle b/app/autodiscovery/providersxml/build.gradle
index f427e7836739b96790f1fc5dca6da358fbd4e6df..18cc6a8682470f1ad52cf33ace9487dc07c7220c 100644
--- a/app/autodiscovery/providersxml/build.gradle
+++ b/app/autodiscovery/providersxml/build.gradle
@@ -16,7 +16,7 @@ dependencies {
testImplementation "com.google.truth:truth:${versions.truth}"
testImplementation "org.mockito:mockito-core:${versions.mockito}"
testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}"
- testImplementation "org.koin:koin-test:${versions.koin}"
+ testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}"
}
android {
diff --git a/app/autodiscovery/providersxml/src/main/res/xml/providers.xml b/app/autodiscovery/providersxml/src/main/res/xml/providers.xml
index fa728c1758da096d2908bb81b643186c77a596d6..c7bee9b7510140b226bb65e3f029c1f7ac642513 100644
--- a/app/autodiscovery/providersxml/src/main/res/xml/providers.xml
+++ b/app/autodiscovery/providersxml/src/main/res/xml/providers.xml
@@ -284,6 +284,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/autodiscovery/srvrecords/build.gradle b/app/autodiscovery/srvrecords/build.gradle
index 565bf3659df6394fdfbefabfaf86c48d21b78d42..2ebb818f17020b4170cc45deb8b2fd62b157fa3f 100644
--- a/app/autodiscovery/srvrecords/build.gradle
+++ b/app/autodiscovery/srvrecords/build.gradle
@@ -17,7 +17,7 @@ dependencies {
testImplementation "com.google.truth:truth:${versions.truth}"
testImplementation "org.mockito:mockito-core:${versions.mockito}"
testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}"
- testImplementation "org.koin:koin-test:${versions.koin}"
+ testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}"
}
android {
diff --git a/app/autodiscovery/thunderbird/build.gradle b/app/autodiscovery/thunderbird/build.gradle
index afad77f1a66f62f78e0b8344e0b8ebd59e30f99c..ed21e968f289f614f7682c7db5425e7b64c47a0e 100644
--- a/app/autodiscovery/thunderbird/build.gradle
+++ b/app/autodiscovery/thunderbird/build.gradle
@@ -17,7 +17,7 @@ dependencies {
testImplementation "com.google.truth:truth:${versions.truth}"
testImplementation "org.mockito:mockito-core:${versions.mockito}"
testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}"
- testImplementation "org.koin:koin-test:${versions.koin}"
+ testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}"
}
android {
diff --git a/app/core/build.gradle b/app/core/build.gradle
index 50526b3e1578357e72697b4573cd7d550f41aa57..8bb6de20722cd8dfd153ac6b4decdf149e85effd 100644
--- a/app/core/build.gradle
+++ b/app/core/build.gradle
@@ -8,7 +8,7 @@ dependencies {
implementation project(':plugins:openpgp-api-lib:openpgp-api')
- api "org.koin:koin-androidx-viewmodel:${versions.koin}"
+ api "io.insert-koin:koin-android:${versions.koin}"
api "androidx.annotation:annotation:${versions.androidxAnnotation}"
@@ -35,7 +35,7 @@ dependencies {
testImplementation "org.mockito:mockito-core:${versions.mockito}"
testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}"
testImplementation "org.jdom:jdom2:2.0.6"
- testImplementation "org.koin:koin-test:${versions.koin}"
+ testImplementation "io.insert-koin:koin-test:${versions.koin}"
}
android {
diff --git a/app/core/src/main/AndroidManifest.xml b/app/core/src/main/AndroidManifest.xml
index 5900dccc24b0a8a8f7c086fc78dae3c83108a1b5..dca0147e8479133a5bf48a146db018e513b7eed9 100644
--- a/app/core/src/main/AndroidManifest.xml
+++ b/app/core/src/main/AndroidManifest.xml
@@ -7,11 +7,21 @@
+
+ tools:node="merge">
+
+
+
+
+
+
diff --git a/app/core/src/main/java/com/fsck/k9/Account.java b/app/core/src/main/java/com/fsck/k9/Account.java
index 4e0a35ebf86d1ce924478185497d964d714b7fd9..5f1fdcdad68fa98a300e19fcdaa8df75e6643352 100644
--- a/app/core/src/main/java/com/fsck/k9/Account.java
+++ b/app/core/src/main/java/com/fsck/k9/Account.java
@@ -11,15 +11,12 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-import android.content.Context;
import android.text.TextUtils;
import com.fsck.k9.backend.api.SyncConfig.ExpungePolicy;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.NetworkType;
import com.fsck.k9.mail.ServerSettings;
-import com.fsck.k9.mailstore.StorageManager;
-import com.fsck.k9.mailstore.StorageManager.StorageProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -119,6 +116,7 @@ public class Account implements BaseAccount {
private FolderMode folderNotifyNewMailMode;
private boolean notifySelfNewMail;
private boolean notifyContactsMailOnly;
+ private boolean ignoreChatMessages;
private String legacyInboxFolder;
private String importedDraftsFolder;
private String importedSentFolder;
@@ -157,9 +155,6 @@ public class Account implements BaseAccount {
private boolean subscribedFoldersOnly;
private int maximumPolledMessageAge;
private int maximumAutoDownloadMessageSize;
- // Tracks if we have sent a notification for this account for
- // current set of fetched messages
- private boolean ringNotified;
private MessageFormat messageFormat;
private boolean messageFormatAuto;
private boolean messageReadReceipt;
@@ -185,6 +180,7 @@ public class Account implements BaseAccount {
private long lastSyncTime;
private long lastFolderListRefreshTime;
private boolean isFinishedSetup = false;
+ private int messagesNotificationChannelVersion;
private boolean changedVisibleLimits = false;
@@ -316,15 +312,6 @@ public class Account implements BaseAccount {
this.alwaysBcc = alwaysBcc;
}
- /* Have we sent a new mail notification on this account */
- public boolean isRingNotified() {
- return ringNotified;
- }
-
- public void setRingNotified(boolean ringNotified) {
- this.ringNotified = ringNotified;
- }
-
public String getLocalStorageProviderId() {
return localStorageProviderId;
}
@@ -621,6 +608,18 @@ public class Account implements BaseAccount {
this.notifySync = notifySync;
}
+ public synchronized int getMessagesNotificationChannelVersion() {
+ return messagesNotificationChannelVersion;
+ }
+
+ public synchronized void setMessagesNotificationChannelVersion(int notificationChannelVersion) {
+ messagesNotificationChannelVersion = notificationChannelVersion;
+ }
+
+ public synchronized void incrementMessagesNotificationChannelVersion() {
+ messagesNotificationChannelVersion++;
+ }
+
public synchronized SortType getSortType() {
return sortType;
}
@@ -680,6 +679,14 @@ public class Account implements BaseAccount {
this.notifyContactsMailOnly = notifyContactsMailOnly;
}
+ public synchronized boolean isIgnoreChatMessages() {
+ return ignoreChatMessages;
+ }
+
+ public synchronized void setIgnoreChatMessages(boolean ignoreChatMessages) {
+ this.ignoreChatMessages = ignoreChatMessages;
+ }
+
public synchronized Expunge getExpungePolicy() {
return expungePolicy;
}
@@ -1034,16 +1041,6 @@ public class Account implements BaseAccount {
return notificationSetting;
}
- /**
- * @return true
if our {@link StorageProvider} is ready. (e.g.
- * card inserted)
- */
- public boolean isAvailable(Context context) {
- String localStorageProviderId = getLocalStorageProviderId();
- boolean storageProviderIsInternalMemory = localStorageProviderId == null;
- return storageProviderIsInternalMemory || StorageManager.getInstance(context).isReady(localStorageProviderId);
- }
-
public synchronized boolean isMarkMessageAsReadOnView() {
return markMessageAsReadOnView;
}
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 c84c699fa14de1f91853fc021eea799ec7105e58..bcee0a8eb90cc9521bcf24e7527429982d06cb25 100644
--- a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt
+++ b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt
@@ -51,7 +51,9 @@ class AccountPreferenceSerializer(
folderNotifyNewMailMode = getEnumStringPref(storage, "$accountUuid.folderNotifyNewMailMode", FolderMode.ALL)
isNotifySelfNewMail = storage.getBoolean("$accountUuid.notifySelfNewMail", true)
isNotifyContactsMailOnly = storage.getBoolean("$accountUuid.notifyContactsMailOnly", false)
+ isIgnoreChatMessages = storage.getBoolean("$accountUuid.ignoreChatMessages", false)
isNotifySync = storage.getBoolean("$accountUuid.notifyMailCheck", false)
+ messagesNotificationChannelVersion = storage.getInt("$accountUuid.messagesNotificationChannelVersion", 0)
deletePolicy = DeletePolicy.fromInt(storage.getInt("$accountUuid.deletePolicy", DeletePolicy.NEVER.setting))
legacyInboxFolder = storage.getString("$accountUuid.inboxFolderName", null)
importedDraftsFolder = storage.getString("$accountUuid.draftsFolderName", null)
@@ -256,7 +258,9 @@ class AccountPreferenceSerializer(
editor.putString("$accountUuid.folderNotifyNewMailMode", folderNotifyNewMailMode.name)
editor.putBoolean("$accountUuid.notifySelfNewMail", isNotifySelfNewMail)
editor.putBoolean("$accountUuid.notifyContactsMailOnly", isNotifyContactsMailOnly)
+ editor.putBoolean("$accountUuid.ignoreChatMessages", isIgnoreChatMessages)
editor.putBoolean("$accountUuid.notifyMailCheck", isNotifySync)
+ editor.putInt("$accountUuid.messagesNotificationChannelVersion", messagesNotificationChannelVersion)
editor.putInt("$accountUuid.deletePolicy", deletePolicy.setting)
editor.putString("$accountUuid.inboxFolderName", legacyInboxFolder)
editor.putString("$accountUuid.draftsFolderName", importedDraftsFolder)
@@ -379,6 +383,8 @@ class AccountPreferenceSerializer(
editor.remove("$accountUuid.lastAutomaticCheckTime")
editor.remove("$accountUuid.notifyNewMail")
editor.remove("$accountUuid.notifySelfNewMail")
+ editor.remove("$accountUuid.ignoreChatMessages")
+ editor.remove("$accountUuid.messagesNotificationChannelVersion")
editor.remove("$accountUuid.deletePolicy")
editor.remove("$accountUuid.draftsFolderName")
editor.remove("$accountUuid.sentFolderName")
@@ -551,6 +557,8 @@ class AccountPreferenceSerializer(
isNotifySync = false
isNotifySelfNewMail = true
isNotifyContactsMailOnly = false
+ isIgnoreChatMessages = false
+ messagesNotificationChannelVersion = 0
folderDisplayMode = FolderMode.NOT_SECOND_CLASS
folderSyncMode = FolderMode.FIRST_CLASS
folderPushMode = FolderMode.NONE
diff --git a/app/core/src/main/java/com/fsck/k9/Clock.java b/app/core/src/main/java/com/fsck/k9/Clock.java
deleted file mode 100644
index a07841144e285579836a013843cbe2d867b87e4e..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/Clock.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.fsck.k9;
-
-/**
- * A class provide the current time (like {@link System#currentTimeMillis()}).
- * It's intended to be mocked out for unit tests.
- */
-public class Clock {
- public static final Clock INSTANCE = new Clock();
-
- protected Clock() {
- }
-
- public long getTime() {
- return System.currentTimeMillis();
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/Clock.kt b/app/core/src/main/java/com/fsck/k9/Clock.kt
new file mode 100644
index 0000000000000000000000000000000000000000..755102351c07ac40ef80d2d6c408d655d8c645c5
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/Clock.kt
@@ -0,0 +1,13 @@
+package com.fsck.k9
+
+/**
+ * An interface to provide the current time.
+ */
+interface Clock {
+ val time: Long
+}
+
+internal class RealClock : Clock {
+ override val time: Long
+ get() = System.currentTimeMillis()
+}
diff --git a/app/core/src/main/java/com/fsck/k9/Core.kt b/app/core/src/main/java/com/fsck/k9/Core.kt
index 003608fe8acc20c787217415087619e62fe561f1..7282a86526de2dc9da6e27d4d8ddc2e16e66b0e5 100644
--- a/app/core/src/main/java/com/fsck/k9/Core.kt
+++ b/app/core/src/main/java/com/fsck/k9/Core.kt
@@ -2,40 +2,39 @@ package com.fsck.k9
import android.content.ComponentName
import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
import android.content.pm.PackageManager
-import android.os.Handler
-import android.os.Looper
import com.fsck.k9.job.K9JobManager
import com.fsck.k9.mail.internet.BinaryTempFileBody
-import com.fsck.k9.service.StorageGoneReceiver
-import java.util.concurrent.SynchronousQueue
-import timber.log.Timber
+import com.fsck.k9.notification.NotificationController
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.koin.core.qualifier.named
object Core : EarlyInit {
private val context: Context by inject()
private val appConfig: AppConfig by inject()
private val jobManager: K9JobManager by inject()
+ private val appCoroutineScope: CoroutineScope by inject(named("AppCoroutineScope"))
+ private val preferences: Preferences by inject()
+ private val notificationController: NotificationController by inject()
/**
* This needs to be called from [Application#onCreate][android.app.Application#onCreate] before calling through
* to the super class's `onCreate` implementation and before initializing the dependency injection library.
*/
- fun earlyInit(context: Context) {
+ fun earlyInit() {
if (K9.DEVELOPER_MODE) {
enableStrictMode()
}
-
- val packageName = context.packageName
- K9.Intents.init(packageName)
}
fun init(context: Context) {
BinaryTempFileBody.setTempDirectory(context.cacheDir)
setServicesEnabled(context)
- registerReceivers(context)
+
+ restoreNotifications()
}
/**
@@ -80,41 +79,10 @@ object Core : EarlyInit {
}
}
- /**
- * Register BroadcastReceivers programmatically because doing it from manifest
- * would make K-9 auto-start. We don't want auto-start because the initialization
- * sequence isn't safe while some events occur (SD card unmount).
- */
- private fun registerReceivers(context: Context) {
- val receiver = StorageGoneReceiver()
- val filter = IntentFilter()
- filter.addAction(Intent.ACTION_MEDIA_EJECT)
- filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED)
- filter.addDataScheme("file")
-
- val queue = SynchronousQueue()
-
- // starting a new thread to handle unmount events
- Thread(
- Runnable {
- Looper.prepare()
- try {
- queue.put(Handler())
- } catch (e: InterruptedException) {
- Timber.e(e)
- }
-
- Looper.loop()
- },
- "Unmount-thread"
- ).start()
-
- try {
- val storageGoneHandler = queue.take()
- context.registerReceiver(receiver, filter, null, storageGoneHandler)
- Timber.i("Registered: unmount receiver")
- } catch (e: InterruptedException) {
- Timber.e(e, "Unable to register unmount receiver")
+ private fun restoreNotifications() {
+ appCoroutineScope.launch(Dispatchers.IO) {
+ val accounts = preferences.accounts
+ notificationController.restoreNewMailNotifications(accounts)
}
}
}
diff --git a/app/core/src/main/java/com/fsck/k9/CoreKoinModules.kt b/app/core/src/main/java/com/fsck/k9/CoreKoinModules.kt
index de460da656119ce617fef30b1c02811c82dff641..d79482d55df87d0417276d70c7e5389a7e58027b 100644
--- a/app/core/src/main/java/com/fsck/k9/CoreKoinModules.kt
+++ b/app/core/src/main/java/com/fsck/k9/CoreKoinModules.kt
@@ -6,12 +6,14 @@ import com.fsck.k9.controller.push.controllerPushModule
import com.fsck.k9.crypto.openPgpModule
import com.fsck.k9.helper.helperModule
import com.fsck.k9.job.jobModule
+import com.fsck.k9.logging.loggingModule
import com.fsck.k9.mailstore.mailStoreModule
import com.fsck.k9.message.extractors.extractorModule
import com.fsck.k9.message.html.htmlModule
import com.fsck.k9.message.quote.quoteModule
import com.fsck.k9.network.connectivityModule
import com.fsck.k9.notification.coreNotificationModule
+import com.fsck.k9.power.powerModule
import com.fsck.k9.preferences.preferencesModule
import com.fsck.k9.search.searchModule
@@ -30,5 +32,7 @@ val coreModules = listOf(
jobModule,
helperModule,
preferencesModule,
- connectivityModule
+ connectivityModule,
+ powerModule,
+ loggingModule
)
diff --git a/app/core/src/main/java/com/fsck/k9/CoreResourceProvider.kt b/app/core/src/main/java/com/fsck/k9/CoreResourceProvider.kt
index 097221557bd17d58ef79c47cc51abb688dab4f11..1c8a9b13e6732124f1083f149879b2df2e09a547 100644
--- a/app/core/src/main/java/com/fsck/k9/CoreResourceProvider.kt
+++ b/app/core/src/main/java/com/fsck/k9/CoreResourceProvider.kt
@@ -6,9 +6,6 @@ interface CoreResourceProvider {
fun defaultSignature(): String
fun defaultIdentityDescription(): String
- fun internalStorageProviderName(): String
- fun externalStorageProviderName(): String
-
fun contactDisplayNamePrefix(): String
fun contactUnknownSender(): String
fun contactUnknownRecipient(): String
@@ -27,8 +24,6 @@ interface CoreResourceProvider {
fun replyHeader(sender: String): String
fun replyHeader(sender: String, sentDate: String): String
- fun searchAllMessagesTitle(): String
- fun searchAllMessagesDetail(): String
fun searchUnifiedInboxTitle(): String
fun searchUnifiedInboxDetail(): String
diff --git a/app/core/src/main/java/com/fsck/k9/DI.kt b/app/core/src/main/java/com/fsck/k9/DI.kt
index 8191e8c71dcd7ebffaf192fcaf3ef6e077073740..00adb26ba187f060f8e1c4026788fd0be8331e61 100644
--- a/app/core/src/main/java/com/fsck/k9/DI.kt
+++ b/app/core/src/main/java/com/fsck/k9/DI.kt
@@ -29,6 +29,10 @@ object DI {
fun get(clazz: Class): T {
return koinGet(clazz)
}
+
+ inline fun get(): T {
+ return koinGet(T::class.java)
+ }
}
interface EarlyInit
diff --git a/app/core/src/main/java/com/fsck/k9/K9.kt b/app/core/src/main/java/com/fsck/k9/K9.kt
index 98a96dc7cc8bf5d77b958fc8a5433249f08d58d8..1fe5fafaa4132876a6166163dd41e3f7cd8632dd 100644
--- a/app/core/src/main/java/com/fsck/k9/K9.kt
+++ b/app/core/src/main/java/com/fsck/k9/K9.kt
@@ -119,15 +119,6 @@ object K9 : EarlyInit {
@JvmStatic
var k9Language = ""
- @JvmStatic
- var appTheme = AppTheme.FOLLOW_SYSTEM
-
- var messageViewTheme = SubTheme.USE_GLOBAL
- var messageComposeTheme = SubTheme.USE_GLOBAL
-
- @JvmStatic
- var isFixedMessageViewTheme = true
-
@JvmStatic
val fontSizes = FontSizes()
@@ -149,6 +140,16 @@ object K9 : EarlyInit {
@JvmStatic
var isConfirmSpam = false
+ @JvmStatic
+ var appTheme = AppTheme.FOLLOW_SYSTEM
+
+ var messageViewTheme = SubTheme.USE_GLOBAL
+ var messageComposeTheme = SubTheme.USE_GLOBAL
+
+ @JvmStatic
+ var isFixedMessageViewTheme = true
+
+
@JvmStatic
var isConfirmDeleteFromNotification = true
@@ -206,6 +207,9 @@ object K9 : EarlyInit {
@JvmStatic
var isShowUnifiedInbox = true
+ @JvmStatic
+ var isShowStarredCount = false
+
@JvmStatic
var isAutoFitWidth: Boolean = false
@@ -271,7 +275,8 @@ object K9 : EarlyInit {
return false
}
- val quietTimeChecker = QuietTimeChecker(Clock.INSTANCE, quietTimeStarts, quietTimeEnds)
+ val clock = DI.get()
+ val quietTimeChecker = QuietTimeChecker(clock, quietTimeStarts, quietTimeEnds)
return quietTimeChecker.isQuietTime
}
@@ -310,6 +315,7 @@ object K9 : EarlyInit {
isUseVolumeKeysForNavigation = storage.getBoolean("useVolumeKeysForNavigation", false)
isUseVolumeKeysForListNavigation = storage.getBoolean("useVolumeKeysForListNavigation", false)
isShowUnifiedInbox = storage.getBoolean("showUnifiedInbox", true)
+ isShowStarredCount = storage.getBoolean("showStarredCount", false)
isMessageListSenderAboveSubject = storage.getBoolean("messageListSenderAboveSubject", false)
isShowMessageListStars = storage.getBoolean("messageListStars", true)
messageListPreviewLines = storage.getInt("messageListPreviewLines", 2)
@@ -346,7 +352,6 @@ object K9 : EarlyInit {
val sortAscendingSetting = storage.getBoolean("sortAscending", Account.DEFAULT_SORT_ASCENDING)
sortAscending[sortType] = sortAscendingSetting
- notificationHideSubject = storage.getEnum("notificationHideSubject", NotificationHideSubject.NEVER)
notificationQuickDeleteBehaviour = storage.getEnum("notificationQuickDelete", NotificationQuickDelete.ALWAYS)
lockScreenNotificationVisibility = storage.getEnum(
@@ -375,7 +380,7 @@ object K9 : EarlyInit {
k9Language = storage.getString("language", "")
- appTheme = AppTheme.FOLLOW_SYSTEM //storage.getEnum("theme", AppTheme.FOLLOW_SYSTEM)
+ appTheme = AppTheme.FOLLOW_SYSTEM // storage.getEnum("theme", AppTheme.FOLLOW_SYSTEM)
messageViewTheme = storage.getEnum("messageViewTheme", SubTheme.USE_GLOBAL)
messageComposeTheme = storage.getEnum("messageComposeTheme", SubTheme.USE_GLOBAL)
@@ -394,10 +399,10 @@ object K9 : EarlyInit {
editor.putBoolean("notificationDuringQuietTimeEnabled", isNotificationDuringQuietTimeEnabled)
editor.putString("quietTimeStarts", quietTimeStarts)
editor.putString("quietTimeEnds", quietTimeEnds)
- editor.putBoolean("isSentSoundEnabled", isSentSoundEnabled)
editor.putBoolean("messageListSenderAboveSubject", isMessageListSenderAboveSubject)
editor.putBoolean("showUnifiedInbox", isShowUnifiedInbox)
+ editor.putBoolean("showStarredCount", isShowStarredCount)
editor.putBoolean("messageListStars", isShowMessageListStars)
editor.putInt("messageListPreviewLines", messageListPreviewLines)
editor.putBoolean("showCorrespondentNames", isShowCorrespondentNames)
diff --git a/app/core/src/main/java/com/fsck/k9/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/KoinModule.kt
index de26547cc9c74d5b310525f60399c921a30a45b4..05c30287cc1146b04577fcce4ad85c4829eafb72 100644
--- a/app/core/src/main/java/com/fsck/k9/KoinModule.kt
+++ b/app/core/src/main/java/com/fsck/k9/KoinModule.kt
@@ -2,13 +2,11 @@ package com.fsck.k9
import android.content.Context
import com.fsck.k9.helper.Contacts
-import com.fsck.k9.mail.power.PowerManager
import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory
import com.fsck.k9.mail.ssl.LocalKeyStore
import com.fsck.k9.mail.ssl.TrustManagerFactory
import com.fsck.k9.mail.ssl.TrustedSocketFactory
import com.fsck.k9.mailstore.LocalStoreProvider
-import com.fsck.k9.power.TracingPowerManager
import com.fsck.k9.setup.ServerNameSuggester
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
@@ -19,7 +17,6 @@ val mainModule = module {
single(named("AppCoroutineScope")) { GlobalScope }
single {
Preferences(
- context = get(),
storagePersister = get(),
localStoreProvider = get(),
accountPreferenceSerializer = get()
@@ -28,13 +25,12 @@ val mainModule = module {
single { get().resources }
single { get().contentResolver }
single { LocalStoreProvider() }
- single { TracingPowerManager.getPowerManager(get()) }
single { Contacts.getInstance(get()) }
single { LocalKeyStore(directoryProvider = get()) }
single { TrustManagerFactory.createInstance(get()) }
single { LocalKeyStoreManager(get()) }
single { DefaultTrustedSocketFactory(get(), get()) }
- single { Clock.INSTANCE }
+ single { RealClock() }
factory { ServerNameSuggester() }
factory { EmailAddressValidator() }
factory { ServerSettingsSerializer() }
diff --git a/app/core/src/main/java/com/fsck/k9/Preferences.kt b/app/core/src/main/java/com/fsck/k9/Preferences.kt
index 0a71b46cfb9b9c0af651ea47692469975b4d682c..18441845ba19456be441f16435ff1a5db98eb7bb 100644
--- a/app/core/src/main/java/com/fsck/k9/Preferences.kt
+++ b/app/core/src/main/java/com/fsck/k9/Preferences.kt
@@ -3,6 +3,7 @@ package com.fsck.k9
import android.content.Context
import androidx.annotation.GuardedBy
import androidx.annotation.RestrictTo
+import com.fsck.k9.helper.sendBlockingSilently
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mailstore.LocalStoreProvider
import com.fsck.k9.preferences.AccountManager
@@ -18,7 +19,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.channels.sendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
@@ -26,7 +26,6 @@ import kotlinx.coroutines.flow.flowOn
import timber.log.Timber
class Preferences internal constructor(
- private val context: Context,
private val storagePersister: StoragePersister,
private val localStoreProvider: LocalStoreProvider,
private val accountPreferenceSerializer: AccountPreferenceSerializer,
@@ -104,9 +103,6 @@ class Preferences internal constructor(
}
}
- val availableAccounts: Collection
- get() = accounts.filter { it.isAvailable(context) }
-
override fun getAccount(accountUuid: String): Account? {
synchronized(accountLock) {
if (accountsMap == null) {
@@ -119,20 +115,21 @@ class Preferences internal constructor(
@OptIn(ExperimentalCoroutinesApi::class)
override fun getAccountFlow(accountUuid: String): Flow {
- return callbackFlow {
- val initialAccount = getAccount(accountUuid) ?: return@callbackFlow
+ return callbackFlow {
+ val initialAccount = getAccount(accountUuid)
+ if (initialAccount == null) {
+ close()
+ return@callbackFlow
+ }
+
send(initialAccount)
val listener = AccountsChangeListener {
val account = getAccount(accountUuid)
if (account != null) {
- try {
- sendBlocking(account)
- } catch (e: Exception) {
- Timber.w(e, "Error while trying to send to channel")
- }
+ sendBlockingSilently(account)
} else {
- channel.close()
+ close()
}
}
addOnAccountsChangeListener(listener)
@@ -144,6 +141,23 @@ class Preferences internal constructor(
.flowOn(backgroundDispatcher)
}
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override fun getAccountsFlow(): Flow> {
+ return callbackFlow {
+ send(accounts)
+
+ val listener = AccountsChangeListener {
+ sendBlockingSilently(accounts)
+ }
+ addOnAccountsChangeListener(listener)
+
+ awaitClose {
+ removeOnAccountsChangeListener(listener)
+ }
+ }.buffer(capacity = Channel.CONFLATED)
+ .flowOn(backgroundDispatcher)
+ }
+
fun newAccount(): Account {
val accountUuid = UUID.randomUUID().toString()
val account = Account(accountUuid)
@@ -176,27 +190,8 @@ class Preferences internal constructor(
notifyAccountsChangeListeners()
}
- var defaultAccount: Account?
- get() {
- return getDefaultAccountOrNull() ?: availableAccounts.firstOrNull()?.also { newDefaultAccount ->
- defaultAccount = newDefaultAccount
- }
- }
- set(account) {
- requireNotNull(account)
-
- createStorageEditor()
- .putString("defaultAccountUuid", account.uuid)
- .commit()
- }
-
- private fun getDefaultAccountOrNull(): Account? {
- return synchronized(accountLock) {
- storage.getString("defaultAccountUuid", null)?.let { defaultAccountUuid ->
- getAccount(defaultAccountUuid)
- }
- }
- }
+ val defaultAccount: Account?
+ get() = accounts.firstOrNull()
fun saveAccount(account: Account) {
ensureAssignedAccountNumber(account)
diff --git a/app/core/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java b/app/core/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java
index ddece0ad9784731695920dc17727a16b60ce2d54..206733fa5807db3e828b00c9b9758560cf425524 100644
--- a/app/core/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java
+++ b/app/core/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java
@@ -5,6 +5,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
+import java.util.Locale;
import android.content.Intent;
import android.os.Bundle;
@@ -94,7 +95,8 @@ public class AutocryptOperations {
List autocryptGossipHeaders, Date effectiveDate) {
Bundle updates = new Bundle();
for (AutocryptGossipHeader autocryptGossipHeader : autocryptGossipHeaders) {
- boolean isAcceptedAddress = gossipAcceptedAddresses.contains(autocryptGossipHeader.addr.toLowerCase());
+ String normalizedAddress = autocryptGossipHeader.addr.toLowerCase(Locale.ROOT);
+ boolean isAcceptedAddress = gossipAcceptedAddresses.contains(normalizedAddress);
if (!isAcceptedAddress) {
continue;
}
@@ -122,7 +124,7 @@ public class AutocryptOperations {
for (Address address : message.getRecipients(recipientType)) {
String addr = address.getAddress();
if (addr != null) {
- result.add(addr.toLowerCase());
+ result.add(addr.toLowerCase(Locale.ROOT));
}
}
}
diff --git a/app/core/src/main/java/com/fsck/k9/controller/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
index eedc4b21723e0fbe85dd7c504a2711b07061b61d..f3e94011fbc368b326c5537af49486b671ef5710 100644
--- a/app/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
+++ b/app/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
@@ -18,7 +18,7 @@ val controllerModule = module {
get(),
get(),
get(),
- get(),
+ get(),
get(),
get(),
get(),
@@ -26,5 +26,11 @@ val controllerModule = module {
get(named("controllerExtensions"))
)
}
- single { DefaultUnreadMessageCountProvider(get(), get(), get(), get()) }
+ single {
+ DefaultMessageCountsProvider(
+ preferences = get(),
+ accountSearchConditions = get(),
+ localStoreProvider = get()
+ )
+ }
}
diff --git a/app/core/src/main/java/com/fsck/k9/controller/MemorizingMessagingListener.java b/app/core/src/main/java/com/fsck/k9/controller/MemorizingMessagingListener.java
index 08d93e459c46e937271e3d302bf798336ecbcf80..20d94a2ef0ab774a7074065c32317bd098cea75e 100644
--- a/app/core/src/main/java/com/fsck/k9/controller/MemorizingMessagingListener.java
+++ b/app/core/src/main/java/com/fsck/k9/controller/MemorizingMessagingListener.java
@@ -102,7 +102,7 @@ class MemorizingMessagingListener extends SimpleMessagingListener {
}
private static String getMemoryKey(Account account, long folderId) {
- return account.getDescription() + ":" + folderId;
+ return account.getUuid() + ":" + folderId;
}
private enum MemorizingState { STARTED, FINISHED, FAILED }
diff --git a/app/core/src/main/java/com/fsck/k9/controller/UnreadMessageCountProvider.kt b/app/core/src/main/java/com/fsck/k9/controller/MessageCountsProvider.kt
similarity index 50%
rename from app/core/src/main/java/com/fsck/k9/controller/UnreadMessageCountProvider.kt
rename to app/core/src/main/java/com/fsck/k9/controller/MessageCountsProvider.kt
index c2371ff0f1045add04fe4c55a2a9c44c340c83cb..4cfe1e1d590c4b32a8efb508e095bb4ffa43c1cb 100644
--- a/app/core/src/main/java/com/fsck/k9/controller/UnreadMessageCountProvider.kt
+++ b/app/core/src/main/java/com/fsck/k9/controller/MessageCountsProvider.kt
@@ -1,6 +1,5 @@
package com.fsck.k9.controller
-import android.content.Context
import com.fsck.k9.Account
import com.fsck.k9.Preferences
import com.fsck.k9.mail.MessagingException
@@ -11,22 +10,19 @@ import com.fsck.k9.search.SearchAccount
import com.fsck.k9.search.getAccounts
import timber.log.Timber
-interface UnreadMessageCountProvider {
- fun getUnreadMessageCount(account: Account): Int
- fun getUnreadMessageCount(searchAccount: SearchAccount): Int
+interface MessageCountsProvider {
+ fun getMessageCounts(account: Account): MessageCounts
+ fun getMessageCounts(searchAccount: SearchAccount): MessageCounts
}
-internal class DefaultUnreadMessageCountProvider(
- private val context: Context,
+data class MessageCounts(val unread: Int, val starred: Int)
+
+internal class DefaultMessageCountsProvider(
private val preferences: Preferences,
private val accountSearchConditions: AccountSearchConditions,
private val localStoreProvider: LocalStoreProvider
-) : UnreadMessageCountProvider {
- override fun getUnreadMessageCount(account: Account): Int {
- if (!account.isAvailable(context)) {
- return 0
- }
-
+) : MessageCountsProvider {
+ override fun getMessageCounts(account: Account): MessageCounts {
return try {
val localStore = localStoreProvider.getInstance(account)
@@ -34,32 +30,35 @@ internal class DefaultUnreadMessageCountProvider(
accountSearchConditions.excludeSpecialFolders(account, search)
accountSearchConditions.limitToDisplayableFolders(account, search)
- localStore.getUnreadMessageCount(search)
+ localStore.getMessageCounts(search)
} catch (e: MessagingException) {
- Timber.e(e, "Unable to getUnreadMessageCount for account: %s", account)
- 0
+ Timber.e(e, "Unable to getMessageCounts for account: %s", account)
+ MessageCounts(0, 0)
}
}
- override fun getUnreadMessageCount(searchAccount: SearchAccount): Int {
+ override fun getMessageCounts(searchAccount: SearchAccount): MessageCounts {
val search = searchAccount.relatedSearch
val accounts = search.getAccounts(preferences)
- var unreadMessageCount = 0
+ var unreadCount = 0
+ var starredCount = 0
for (account in accounts) {
- unreadMessageCount += getUnreadMessageCountWithLocalSearch(account, search)
+ val accountMessageCount = getMessageCountsWithLocalSearch(account, search)
+ unreadCount += accountMessageCount.unread
+ starredCount += accountMessageCount.starred
}
- return unreadMessageCount
+ return MessageCounts(unreadCount, starredCount)
}
- private fun getUnreadMessageCountWithLocalSearch(account: Account, search: LocalSearch): Int {
+ private fun getMessageCountsWithLocalSearch(account: Account, search: LocalSearch): MessageCounts {
return try {
val localStore = localStoreProvider.getInstance(account)
- localStore.getUnreadMessageCount(search)
+ localStore.getMessageCounts(search)
} catch (e: MessagingException) {
- Timber.e(e, "Unable to getUnreadMessageCount for account: %s", account)
- 0
+ Timber.e(e, "Unable to getMessageCounts for account: %s", account)
+ MessageCounts(0, 0)
}
}
}
diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessageReference.java b/app/core/src/main/java/com/fsck/k9/controller/MessageReference.java
deleted file mode 100644
index 15701f5cae340f1f7795c6217924735d6665d2b0..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/controller/MessageReference.java
+++ /dev/null
@@ -1,139 +0,0 @@
-package com.fsck.k9.controller;
-
-
-import java.util.StringTokenizer;
-
-import androidx.annotation.Nullable;
-
-import com.fsck.k9.mail.Flag;
-import com.fsck.k9.mail.filter.Base64;
-
-import org.jetbrains.annotations.NotNull;
-
-import static com.fsck.k9.helper.Preconditions.checkNotNull;
-
-
-public class MessageReference {
- private static final char IDENTITY_VERSION_2 = '#';
- private static final String IDENTITY_SEPARATOR = ":";
-
-
- private final String accountUuid;
- private final long folderId;
- private final String uid;
- private final Flag flag;
-
-
- @Nullable
- public static MessageReference parse(String identity) {
- if (identity == null || identity.length() < 1 || identity.charAt(0) != IDENTITY_VERSION_2) {
- return null;
- }
-
- StringTokenizer tokens = new StringTokenizer(identity.substring(2), IDENTITY_SEPARATOR, false);
- if (tokens.countTokens() < 3) {
- return null;
- }
-
- String accountUuid = Base64.decode(tokens.nextToken());
- long folderId = Long.parseLong(Base64.decode(tokens.nextToken()));
- String uid = Base64.decode(tokens.nextToken());
-
- if (!tokens.hasMoreTokens()) {
- return new MessageReference(accountUuid, folderId, uid, null);
- }
-
- Flag flag;
- try {
- flag = Flag.valueOf(tokens.nextToken());
- } catch (IllegalArgumentException e) {
- return null;
- }
-
- return new MessageReference(accountUuid, folderId, uid, flag);
- }
-
- public MessageReference(String accountUuid, long folderId, String uid, Flag flag) {
- this.accountUuid = checkNotNull(accountUuid);
- this.folderId = folderId;
- this.uid = checkNotNull(uid);
- this.flag = flag;
- }
-
- public String toIdentityString() {
- StringBuilder refString = new StringBuilder();
-
- refString.append(IDENTITY_VERSION_2);
- refString.append(IDENTITY_SEPARATOR);
- refString.append(Base64.encode(accountUuid));
- refString.append(IDENTITY_SEPARATOR);
- refString.append(Base64.encode(Long.toString(folderId)));
- refString.append(IDENTITY_SEPARATOR);
- refString.append(Base64.encode(uid));
- if (flag != null) {
- refString.append(IDENTITY_SEPARATOR);
- refString.append(flag.name());
- }
-
- return refString.toString();
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof MessageReference)) {
- return false;
- }
- MessageReference other = (MessageReference) o;
- return equals(other.accountUuid, other.folderId, other.uid);
- }
-
- public boolean equals(String accountUuid, long folderId, String uid) {
- return this.accountUuid.equals(accountUuid) && this.folderId == folderId && this.uid.equals(uid);
- }
-
- @Override
- public int hashCode() {
- final int MULTIPLIER = 31;
-
- int result = 1;
- result = MULTIPLIER * result + accountUuid.hashCode();
- result = MULTIPLIER * result + (int) (folderId ^ (folderId >>> 32));
- result = MULTIPLIER * result + uid.hashCode();
- return result;
- }
-
- @NotNull
- @Override
- public String toString() {
- return "MessageReference{" +
- "accountUuid='" + accountUuid + '\'' +
- ", folderId='" + folderId + '\'' +
- ", uid='" + uid + '\'' +
- ", flag=" + flag +
- '}';
- }
-
- public String getAccountUuid() {
- return accountUuid;
- }
-
- public long getFolderId() {
- return folderId;
- }
-
- public String getUid() {
- return uid;
- }
-
- public Flag getFlag() {
- return flag;
- }
-
- public MessageReference withModifiedUid(String newUid) {
- return new MessageReference(accountUuid, folderId, newUid, flag);
- }
-
- public MessageReference withModifiedFlag(Flag newFlag) {
- return new MessageReference(accountUuid, folderId, uid, newFlag);
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessageReference.kt b/app/core/src/main/java/com/fsck/k9/controller/MessageReference.kt
new file mode 100644
index 0000000000000000000000000000000000000000..13ae584a8c8c483784a9702d83c6623436634756
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/controller/MessageReference.kt
@@ -0,0 +1,52 @@
+package com.fsck.k9.controller
+
+import com.fsck.k9.mail.filter.Base64
+import java.util.StringTokenizer
+
+data class MessageReference(
+ val accountUuid: String,
+ val folderId: Long,
+ val uid: String
+) {
+ fun toIdentityString(): String {
+ return buildString {
+ append(IDENTITY_VERSION_2)
+ append(IDENTITY_SEPARATOR)
+ append(Base64.encode(accountUuid))
+ append(IDENTITY_SEPARATOR)
+ append(Base64.encode(folderId.toString()))
+ append(IDENTITY_SEPARATOR)
+ append(Base64.encode(uid))
+ }
+ }
+
+ fun equals(accountUuid: String, folderId: Long, uid: String): Boolean {
+ return this.accountUuid == accountUuid && this.folderId == folderId && this.uid == uid
+ }
+
+ fun withModifiedUid(newUid: String): MessageReference {
+ return copy(uid = newUid)
+ }
+
+ companion object {
+ private const val IDENTITY_VERSION_2 = '#'
+ private const val IDENTITY_SEPARATOR = ":"
+
+ @JvmStatic
+ fun parse(identity: String?): MessageReference? {
+ if (identity == null || identity.isEmpty() || identity[0] != IDENTITY_VERSION_2) {
+ return null
+ }
+
+ val tokens = StringTokenizer(identity.substring(2), IDENTITY_SEPARATOR, false)
+ if (tokens.countTokens() < 3) {
+ return null
+ }
+
+ val accountUuid = Base64.decode(tokens.nextToken())
+ val folderId = Base64.decode(tokens.nextToken()).toLong()
+ val uid = Base64.decode(tokens.nextToken())
+ return MessageReference(accountUuid, folderId, uid)
+ }
+ }
+}
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 4993b9b667aec548925b1446210dd074197ff965..8b430664496933eb4e31074cee7546f271d23702 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
@@ -23,7 +23,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import android.annotation.SuppressLint;
import android.content.Context;
-import android.os.PowerManager;
import android.os.Process;
import android.os.SystemClock;
@@ -64,6 +63,10 @@ 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;
+import com.fsck.k9.mail.power.PowerManager;
+import com.fsck.k9.mail.power.WakeLock;
+import com.fsck.k9.mailstore.FolderDetailsAccessor;
import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.LocalStore;
@@ -75,11 +78,8 @@ import com.fsck.k9.mailstore.OutboxStateRepository;
import com.fsck.k9.mailstore.SaveMessageData;
import com.fsck.k9.mailstore.SaveMessageDataCreator;
import com.fsck.k9.mailstore.SendState;
-import com.fsck.k9.mailstore.UnavailableStorageException;
import com.fsck.k9.notification.NotificationController;
import com.fsck.k9.notification.NotificationStrategy;
-import com.fsck.k9.power.TracingPowerManager;
-import com.fsck.k9.power.TracingPowerManager.TracingWakeLock;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchAccount;
import org.jetbrains.annotations.NotNull;
@@ -125,8 +125,9 @@ public class MessagingController {
private final Set listeners = new CopyOnWriteArraySet<>();
private final ExecutorService threadPool = Executors.newCachedThreadPool();
private final MemorizingMessagingListener memorizingMessagingListener = new MemorizingMessagingListener();
- private final UnreadMessageCountProvider unreadMessageCountProvider;
+ private final MessageCountsProvider messageCountsProvider;
private final DraftOperations draftOperations;
+ private final NotificationOperations notificationOperations;
private MessagingListener checkMailListener = null;
@@ -140,14 +141,14 @@ public class MessagingController {
MessagingController(Context context, NotificationController notificationController,
NotificationStrategy notificationStrategy, LocalStoreProvider localStoreProvider,
- UnreadMessageCountProvider unreadMessageCountProvider, BackendManager backendManager,
+ MessageCountsProvider messageCountsProvider, BackendManager backendManager,
Preferences preferences, MessageStoreManager messageStoreManager,
SaveMessageDataCreator saveMessageDataCreator, List controllerExtensions) {
this.context = context;
this.notificationController = notificationController;
this.notificationStrategy = notificationStrategy;
this.localStoreProvider = localStoreProvider;
- this.unreadMessageCountProvider = unreadMessageCountProvider;
+ this.messageCountsProvider = messageCountsProvider;
this.backendManager = backendManager;
this.preferences = preferences;
this.messageStoreManager = messageStoreManager;
@@ -166,6 +167,7 @@ public class MessagingController {
initializeControllerExtensions(controllerExtensions);
draftOperations = new DraftOperations(this, messageStoreManager, saveMessageDataCreator);
+ notificationOperations = new NotificationOperations(notificationController, preferences, messageStoreManager);
}
private void initializeControllerExtensions(List controllerExtensions) {
@@ -214,23 +216,7 @@ public class MessagingController {
command.sequence,
command.isForegroundPriority ? "foreground" : "background");
- try {
- command.runnable.run();
- } catch (UnavailableAccountException e) {
- // retry later
- new Thread() {
- @Override
- public void run() {
- try {
- sleep(30 * 1000);
- queuedCommands.put(command);
- } catch (InterruptedException e) {
- Timber.e("Interrupted while putting a pending command for an unavailable account " +
- "back into the queue. THIS SHOULD NEVER HAPPEN.");
- }
- }
- }.start();
- }
+ command.runnable.run();
Timber.i(" Command '%s' completed", command.description);
}
@@ -281,23 +267,22 @@ public class MessagingController {
}
}
- private String getFolderServerId(Account account, long folderId) throws MessagingException {
- LocalStore localStore = getLocalStoreOrThrow(account);
- return localStore.getFolderServerId(folderId);
- }
-
- private long getFolderId(Account account, String folderServerId) throws MessagingException {
- LocalStore localStore = getLocalStoreOrThrow(account);
- return localStore.getFolderId(folderServerId);
+ private String getFolderServerId(Account account, long folderId) {
+ MessageStore messageStore = messageStoreManager.getMessageStore(account);
+ String folderServerId = messageStore.getFolderServerId(folderId);
+ if (folderServerId == null) {
+ throw new IllegalStateException("Folder not found (ID: " + folderId + ")");
+ }
+ return folderServerId;
}
- private long getFolderIdOrThrow(Account account, String folderServerId) {
- LocalStore localStore = getLocalStoreOrThrow(account);
- try {
- return localStore.getFolderId(folderServerId);
- } catch (MessagingException e) {
- throw new IllegalStateException(e);
+ private long getFolderId(Account account, String folderServerId) {
+ MessageStore messageStore = messageStoreManager.getMessageStore(account);
+ Long folderId = messageStore.getFolderId(folderServerId);
+ if (folderId == null) {
+ throw new IllegalStateException("Folder not found (server ID: " + folderServerId + ")");
}
+ return folderId;
}
public void addListener(MessagingListener listener) {
@@ -390,6 +375,12 @@ public class MessagingController {
public void refreshFolderListSynchronous(Account account) {
try {
+ ServerSettings serverSettings = account.getIncomingServerSettings();
+ if (serverSettings.isMissingCredentials()) {
+ handleAuthenticationFailure(account, true);
+ return;
+ }
+
Backend backend = getBackend(account);
backend.refreshFolderList();
@@ -591,17 +582,17 @@ public class MessagingController {
*/
public void synchronizeMailbox(Account account, long folderId, MessagingListener listener) {
putBackground("synchronizeMailbox", listener, () ->
- synchronizeMailboxSynchronous(account, folderId, listener)
+ synchronizeMailboxSynchronous(account, folderId, listener, new NotificationState())
);
}
- public void synchronizeMailboxBlocking(Account account, String folderServerId) throws MessagingException {
+ public void synchronizeMailboxBlocking(Account account, String folderServerId) {
long folderId = getFolderId(account, folderServerId);
final CountDownLatch latch = new CountDownLatch(1);
putBackground("synchronizeMailbox", null, () -> {
try {
- synchronizeMailboxSynchronous(account, folderId, null);
+ synchronizeMailboxSynchronous(account, folderId, null, new NotificationState());
} finally {
latch.countDown();
}
@@ -621,11 +612,12 @@ public class MessagingController {
* TODO Break this method up into smaller chunks.
*/
@VisibleForTesting
- void synchronizeMailboxSynchronous(Account account, long folderId, MessagingListener listener) {
+ void synchronizeMailboxSynchronous(Account account, long folderId, MessagingListener listener,
+ NotificationState notificationState) {
refreshFolderListIfStale(account);
Backend backend = getBackend(account);
- syncFolder(account, folderId, listener, backend);
+ syncFolder(account, folderId, listener, backend, notificationState);
}
private void refreshFolderListIfStale(Account account) {
@@ -640,7 +632,14 @@ public class MessagingController {
}
}
- private void syncFolder(Account account, long folderId, MessagingListener listener, Backend backend) {
+ private void syncFolder(Account account, long folderId, MessagingListener listener, Backend backend,
+ NotificationState notificationState) {
+ ServerSettings serverSettings = account.getIncomingServerSettings();
+ if (serverSettings.isMissingCredentials()) {
+ handleAuthenticationFailure(account, true);
+ return;
+ }
+
Exception commandException = null;
try {
processPendingCommandsSynchronous(account);
@@ -664,9 +663,14 @@ public class MessagingController {
return;
}
+ MessageStore messageStore = messageStoreManager.getMessageStore(account);
+ Long lastChecked = messageStore.getFolder(folderId, FolderDetailsAccessor::getLastChecked);
+ boolean suppressNotifications = lastChecked == null;
+
String folderServerId = localFolder.getServerId();
SyncConfig syncConfig = createSyncConfig(account);
- ControllerSyncListener syncListener = new ControllerSyncListener(account, listener);
+ ControllerSyncListener syncListener =
+ new ControllerSyncListener(account, listener, suppressNotifications, notificationState);
backend.sync(folderServerId, syncConfig, syncListener);
@@ -725,10 +729,6 @@ public class MessagingController {
public void run() {
try {
processPendingCommandsSynchronous(account);
- } catch (UnavailableStorageException e) {
- Timber.i("Failed to process pending command because storage is not available - " +
- "trying again later.");
- throw new UnavailableAccountException(e);
} catch (MessagingException me) {
Timber.e(me, "processPendingCommands");
@@ -1318,7 +1318,7 @@ public class MessagingController {
localFolder.fetch(Collections.singletonList(message), fp, null);
notificationController.removeNewMailNotification(account, message.makeMessageReference());
- markMessageAsReadOnView(account, message);
+ markMessageAsOpened(account, message);
return message;
}
@@ -1341,15 +1341,39 @@ public class MessagingController {
return message;
}
- private void markMessageAsReadOnView(Account account, LocalMessage message)
- throws MessagingException {
+ private void markMessageAsOpened(Account account, LocalMessage message) throws MessagingException {
+ if (!message.isSet(Flag.SEEN)) {
+ if (account.isMarkMessageAsReadOnView()) {
+ markMessageAsReadOnView(account, message);
+ } else {
+ // Marking a message as read will automatically mark it as "not new". But if we don't mark the message
+ // as read on opening, we have to manually mark it as "not new".
+ markMessageAsNotNew(account, message);
+ }
+ }
+ }
- if (account.isMarkMessageAsReadOnView() && !message.isSet(Flag.SEEN)) {
- List messageIds = Collections.singletonList(message.getDatabaseId());
- setFlag(account, messageIds, Flag.SEEN, true);
+ private void markMessageAsReadOnView(Account account, LocalMessage message) throws MessagingException {
+ List messageIds = Collections.singletonList(message.getDatabaseId());
+ setFlag(account, messageIds, Flag.SEEN, true);
- message.setFlagInternal(Flag.SEEN, true);
- }
+ message.setFlagInternal(Flag.SEEN, true);
+ }
+
+ private void markMessageAsNotNew(Account account, LocalMessage message) {
+ MessageStore messageStore = messageStoreManager.getMessageStore(account);
+ long folderId = message.getFolder().getDatabaseId();
+ String messageServerId = message.getUid();
+ messageStore.setNewMessageState(folderId, messageServerId, false);
+ }
+
+ public void clearNewMessages(Account account) {
+ put("clearNewMessages", null, () -> clearNewMessagesBlocking(account));
+ }
+
+ private void clearNewMessagesBlocking(Account account) {
+ MessageStore messageStore = messageStoreManager.getMessageStore(account);
+ messageStore.clearNewMessageState();
}
public void loadAttachment(final Account account, final LocalMessage message, final Part part,
@@ -1426,13 +1450,6 @@ public class MessagingController {
backend.sendMessage(message);
}
- public void sendPendingMessages(MessagingListener listener) {
- for (Account account : preferences.getAvailableAccounts()) {
- sendPendingMessages(account, listener);
- }
- }
-
-
/**
* Attempt to send any messages that are sitting in the Outbox.
*/
@@ -1441,9 +1458,6 @@ public class MessagingController {
putBackground("sendPendingMessages", listener, new Runnable() {
@Override
public void run() {
- if (!account.isAvailable(context)) {
- throw new UnavailableAccountException();
- }
if (messagesPendingSend(account)) {
showSendingNotificationIfNecessary(account);
@@ -1471,21 +1485,14 @@ public class MessagingController {
}
private boolean messagesPendingSend(final Account account) {
- try {
- LocalFolder localFolder = localStoreProvider.getInstance(account).getFolder(account.getOutboxFolderId());
- if (!localFolder.exists()) {
- return false;
- }
-
- localFolder.open();
-
- if (localFolder.getMessageCount() > 0) {
- return true;
- }
- } catch (Exception e) {
- Timber.e(e, "Exception while checking for unsent messages");
+ Long outboxFolderId = account.getOutboxFolderId();
+ if (outboxFolderId == null) {
+ Timber.w("Could not get Outbox folder ID from Account");
+ return false;
}
- return false;
+
+ MessageStore messageStore = messageStoreManager.getMessageStore(account);
+ return messageStore.getMessageCount(outboxFolderId) > 0;
}
/**
@@ -1496,6 +1503,12 @@ public class MessagingController {
Exception lastFailure = null;
boolean wasPermanentFailure = false;
try {
+ ServerSettings serverSettings = account.getOutgoingServerSettings();
+ if (serverSettings.isMissingCredentials()) {
+ handleAuthenticationFailure(account, false);
+ return;
+ }
+
LocalStore localStore = localStoreProvider.getInstance(account);
OutboxStateRepository outboxStateRepository = localStore.getOutboxStateRepository();
LocalFolder localFolder = localStore.getFolder(account.getOutboxFolderId());
@@ -1539,7 +1552,7 @@ public class MessagingController {
OutboxState outboxState = outboxStateRepository.getOutboxState(messageId);
if (outboxState.getSendState() != SendState.READY) {
- Timber.v("Skipping sending message " + message.getUid());
+ Timber.v("Skipping sending message %s", message.getUid());
notificationController.showSendFailedNotification(account,
new MessagingException(message.getSubject()));
continue;
@@ -1622,9 +1635,6 @@ public class MessagingController {
notificationController.showSendFailedNotification(account, lastFailure);
}
}
- } catch (UnavailableStorageException e) {
- Timber.i("Failed to send pending messages because storage is not available - trying again later.");
- throw new UnavailableAccountException(e);
} catch (Exception e) {
Timber.v(e, "Failed to send pending messages");
} finally {
@@ -1658,6 +1668,10 @@ public class MessagingController {
processPendingCommands(account);
}
}
+
+ for (MessagingListener listener : getListeners()) {
+ listener.folderStatusChanged(account, account.getOutboxFolderId());
+ }
}
private void handleSendFailure(Account account, LocalFolder localFolder, Message message, Exception exception)
@@ -1678,11 +1692,13 @@ public class MessagingController {
}
public int getUnreadMessageCount(Account account) {
- return unreadMessageCountProvider.getUnreadMessageCount(account);
+ MessageCounts messageCounts = messageCountsProvider.getMessageCounts(account);
+ return messageCounts.getUnread();
}
public int getUnreadMessageCount(SearchAccount searchAccount) {
- return unreadMessageCountProvider.getUnreadMessageCount(searchAccount);
+ MessageCounts messageCounts = messageCountsProvider.getMessageCounts(searchAccount);
+ return messageCounts.getUnread();
}
public int getFolderUnreadMessageCount(Account account, Long folderId) throws MessagingException {
@@ -1888,9 +1904,6 @@ public class MessagingController {
}
processPendingCommands(account);
- } catch (UnavailableStorageException e) {
- Timber.i("Failed to move/copy message because storage is not available - trying again later.");
- throw new UnavailableAccountException(e);
} catch (MessagingException me) {
throw new RuntimeException("Error moving message", me);
}
@@ -1940,7 +1953,7 @@ public class MessagingController {
localFolder.open();
String uid = localFolder.getMessageUidById(id);
if (uid != null) {
- MessageReference messageReference = new MessageReference(account.getUuid(), folderId, uid, null);
+ MessageReference messageReference = new MessageReference(account.getUuid(), folderId, uid);
deleteMessage(messageReference);
}
} catch (MessagingException me) {
@@ -2135,9 +2148,6 @@ public class MessagingController {
}
unsuppressMessages(account, messages);
- } catch (UnavailableStorageException e) {
- Timber.i("Failed to delete message because storage is not available - trying again later.");
- throw new UnavailableAccountException(e);
} catch (MessagingException me) {
throw new RuntimeException("Error deleting message from local store.", me);
}
@@ -2207,9 +2217,6 @@ public class MessagingController {
queuePendingCommand(account, command);
processPendingCommands(account);
}
- } catch (UnavailableStorageException e) {
- Timber.i("Failed to empty trash because storage is not available - trying again later.");
- throw new UnavailableAccountException(e);
} catch (Exception e) {
Timber.e(e, "emptyTrash failed");
}
@@ -2229,9 +2236,6 @@ public class MessagingController {
LocalFolder localFolder = localStoreProvider.getInstance(account).getFolder(folderId);
localFolder.open();
localFolder.clearAllMessages();
- } catch (UnavailableStorageException e) {
- Timber.i("Failed to clear folder because storage is not available - trying again later.");
- throw new UnavailableAccountException(e);
} catch (Exception e) {
Timber.e(e, "clearFolder failed");
}
@@ -2298,15 +2302,16 @@ public class MessagingController {
final boolean useManualWakeLock,
final MessagingListener listener) {
- TracingWakeLock twakeLock = null;
+ final WakeLock wakeLock;
if (useManualWakeLock) {
- TracingPowerManager pm = TracingPowerManager.getPowerManager(context);
+ PowerManager pm = DI.get(PowerManager.class);
- twakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "K9 MessagingController.checkMail");
- twakeLock.setReferenceCounted(false);
- twakeLock.acquire(K9.MANUAL_WAKE_LOCK_TIMEOUT);
+ wakeLock = pm.newWakeLock("K9 MessagingController.checkMail");
+ wakeLock.setReferenceCounted(false);
+ wakeLock.acquire(K9.MANUAL_WAKE_LOCK_TIMEOUT);
+ } else {
+ wakeLock = null;
}
- final TracingWakeLock wakeLock = twakeLock;
for (MessagingListener l : getListeners(listener)) {
l.checkMailStarted(context, account);
@@ -2323,7 +2328,7 @@ public class MessagingController {
accounts = new ArrayList<>(1);
accounts.add(account);
} else {
- accounts = preferences.getAvailableAccounts();
+ accounts = preferences.getAccounts();
}
for (final Account account : accounts) {
@@ -2357,14 +2362,9 @@ public class MessagingController {
private void checkMailForAccount(final Context context, final Account account,
final boolean ignoreLastCheckedTime,
final MessagingListener listener) {
- if (!account.isAvailable(context)) {
- Timber.i("Skipping synchronizing unavailable account %s", account.getDescription());
- return;
- }
-
Timber.i("Synchronizing account %s", account.getDescription());
- account.setRingNotified(false);
+ NotificationState notificationState = new NotificationState();
sendPendingMessages(account, listener);
@@ -2404,7 +2404,7 @@ public class MessagingController {
continue;
}
- synchronizeFolder(account, folder, ignoreLastCheckedTime, listener);
+ synchronizeFolder(account, folder, ignoreLastCheckedTime, listener, notificationState);
}
} catch (MessagingException e) {
Timber.e(e, "Unable to synchronize account %s", account.getName());
@@ -2414,9 +2414,10 @@ public class MessagingController {
public void run() {
Timber.v("Clearing notification flag for %s", account.getDescription());
- account.setRingNotified(false);
+ clearFetchingMailNotification(account);
+
if (getUnreadMessageCount(account) == 0) {
- notificationController.clearNewMailNotifications(account);
+ notificationController.clearNewMailNotifications(account, false);
}
}
}
@@ -2427,14 +2428,14 @@ public class MessagingController {
}
private void synchronizeFolder(Account account, LocalFolder folder, boolean ignoreLastCheckedTime,
- MessagingListener listener) {
+ MessagingListener listener, NotificationState notificationState) {
putBackground("sync" + folder.getServerId(), null, () -> {
- synchronizeFolderInBackground(account, folder, ignoreLastCheckedTime, listener);
+ synchronizeFolderInBackground(account, folder, ignoreLastCheckedTime, listener, notificationState);
});
}
private void synchronizeFolderInBackground(Account account, LocalFolder folder, boolean ignoreLastCheckedTime,
- MessagingListener listener) {
+ MessagingListener listener, NotificationState notificationState) {
Timber.v("Folder %s was last synced @ %tc", folder.getServerId(), folder.getLastChecked());
if (!ignoreLastCheckedTime) {
@@ -2457,12 +2458,9 @@ public class MessagingController {
try {
showFetchingMailNotificationIfNecessary(account, folder);
try {
- synchronizeMailboxSynchronous(account, folder.getDatabaseId(), listener);
-
- long now = System.currentTimeMillis();
- folder.setLastChecked(now);
+ synchronizeMailboxSynchronous(account, folder.getDatabaseId(), listener, notificationState);
} finally {
- clearFetchingMailNotificationIfNecessary(account);
+ showEmptyFetchingMailNotificationIfNecessary(account);
}
} catch (Exception e) {
Timber.e(e, "Exception while processing folder %s:%s", account.getDescription(), folder.getServerId());
@@ -2475,28 +2473,28 @@ public class MessagingController {
}
}
- private void clearFetchingMailNotificationIfNecessary(Account account) {
+ private void showEmptyFetchingMailNotificationIfNecessary(Account account) {
if (account.isNotifySync()) {
- notificationController.clearFetchingMailNotification(account);
+ notificationController.showEmptyFetchingMailNotification(account);
}
}
+ private void clearFetchingMailNotification(Account account) {
+ notificationController.clearFetchingMailNotification(account);
+ }
public void compact(final Account account, final MessagingListener ml) {
putBackground("compact:" + account.getDescription(), ml, new Runnable() {
@Override
public void run() {
try {
- LocalStore localStore = localStoreProvider.getInstance(account);
- long oldSize = localStore.getSize();
- localStore.compact();
- long newSize = localStore.getSize();
+ 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 (UnavailableStorageException e) {
- Timber.i("Failed to compact account because storage is not available - trying again later.");
- throw new UnavailableAccountException(e);
} catch (Exception e) {
Timber.e(e, "Failed to compact account %s", account.getDescription());
}
@@ -2504,54 +2502,8 @@ public class MessagingController {
});
}
- public void clear(final Account account, final MessagingListener ml) {
- putBackground("clear:" + account.getDescription(), ml, new Runnable() {
- @Override
- public void run() {
- try {
- LocalStore localStore = localStoreProvider.getInstance(account);
- long oldSize = localStore.getSize();
- localStore.clear();
- localStore.resetVisibleLimits(account.getDisplayCount());
- long newSize = localStore.getSize();
- for (MessagingListener l : getListeners(ml)) {
- l.accountSizeChanged(account, oldSize, newSize);
- }
- } catch (UnavailableStorageException e) {
- Timber.i("Failed to clear account because storage is not available - trying again later.");
- throw new UnavailableAccountException(e);
- } catch (Exception e) {
- Timber.e(e, "Failed to clear account %s", account.getDescription());
- }
- }
- });
- }
-
- public void recreate(final Account account, final MessagingListener ml) {
- putBackground("recreate:" + account.getDescription(), ml, new Runnable() {
- @Override
- public void run() {
- try {
- LocalStore localStore = localStoreProvider.getInstance(account);
- long oldSize = localStore.getSize();
- localStore.recreate();
- localStore.resetVisibleLimits(account.getDisplayCount());
- long newSize = localStore.getSize();
- for (MessagingListener l : getListeners(ml)) {
- l.accountSizeChanged(account, oldSize, newSize);
- }
- } catch (UnavailableStorageException e) {
- Timber.i("Failed to recreate an account because storage is not available - trying again later.");
- throw new UnavailableAccountException(e);
- } catch (Exception e) {
- Timber.e(e, "Failed to recreate account %s", account.getDescription());
- }
- }
- });
- }
-
public void deleteAccount(Account account) {
- notificationController.clearNewMailNotifications(account);
+ notificationController.clearNewMailNotifications(account, false);
memorizingMessagingListener.removeAccount(account);
}
@@ -2607,8 +2559,14 @@ public class MessagingController {
}
}
+ public void clearNotifications(LocalSearch search) {
+ put("clearNotifications", null, () -> {
+ notificationOperations.clearNotifications(search);
+ });
+ }
+
public void cancelNotificationsForAccount(Account account) {
- notificationController.clearNewMailNotifications(account);
+ notificationController.clearNewMailNotifications(account, true);
}
public void cancelNotificationForMessage(Account account, MessageReference messageReference) {
@@ -2696,21 +2654,23 @@ public class MessagingController {
private final Account account;
private final MessagingListener listener;
private final LocalStore localStore;
- private final int previousUnreadMessageCount;
+ private final boolean suppressNotifications;
+ private final NotificationState notificationState;
boolean syncFailed = false;
- ControllerSyncListener(Account account, MessagingListener listener) {
+ ControllerSyncListener(Account account, MessagingListener listener, boolean suppressNotifications,
+ NotificationState notificationState) {
this.account = account;
this.listener = listener;
+ this.suppressNotifications = suppressNotifications;
+ this.notificationState = notificationState;
this.localStore = getLocalStoreOrThrow(account);
-
- previousUnreadMessageCount = getUnreadMessageCount(account);
}
@Override
public void syncStarted(@NotNull String folderServerId) {
- long folderId = getFolderIdOrThrow(account, folderServerId);
+ long folderId = getFolderId(account, folderServerId);
for (MessagingListener messagingListener : getListeners(listener)) {
messagingListener.synchronizeMailboxStarted(account, folderId);
}
@@ -2746,7 +2706,7 @@ public class MessagingController {
@Override
public void syncProgress(@NotNull String folderServerId, int completed, int total) {
- long folderId = getFolderIdOrThrow(account, folderServerId);
+ long folderId = getFolderId(account, folderServerId);
for (MessagingListener messagingListener : getListeners(listener)) {
messagingListener.synchronizeMailboxProgress(account, folderId, completed, total);
}
@@ -2759,10 +2719,13 @@ public class MessagingController {
// Send a notification of this message
LocalMessage message = loadMessage(folderServerId, messageServerId);
LocalFolder localFolder = message.getFolder();
- if (notificationStrategy.shouldNotifyForMessage(account, localFolder, message, isOldMessage)) {
+ if (!suppressNotifications &&
+ notificationStrategy.shouldNotifyForMessage(account, localFolder, message, isOldMessage)) {
Timber.v("Creating notification for message %s:%s", localFolder.getName(), message.getUid());
// Notify with the localMessage so that we don't have to recalculate the content preview.
- notificationController.addNewMailNotification(account, message, previousUnreadMessageCount);
+ boolean silent = notificationState.wasNotified();
+ notificationController.addNewMailNotification(account, message, silent);
+ notificationState.setWasNotified(true);
}
if (!message.isSet(Flag.SEEN)) {
@@ -2777,6 +2740,11 @@ public class MessagingController {
for (MessagingListener messagingListener : getListeners(listener)) {
messagingListener.synchronizeMailboxRemovedMessage(account, folderServerId, messageServerId);
}
+
+ String accountUuid = account.getUuid();
+ long folderId = getFolderId(account, folderServerId);
+ MessageReference messageReference = new MessageReference(accountUuid, folderId, messageServerId);
+ notificationController.removeNewMailNotification(account, messageReference);
}
@Override
@@ -2801,7 +2769,7 @@ public class MessagingController {
@Override
public void syncFinished(@NotNull String folderServerId) {
- long folderId = getFolderIdOrThrow(account, folderServerId);
+ long folderId = getFolderId(account, folderServerId);
for (MessagingListener messagingListener : getListeners(listener)) {
messagingListener.synchronizeMailboxFinished(account, folderId);
}
@@ -2817,7 +2785,7 @@ public class MessagingController {
notifyUserIfCertificateProblem(account, exception, true);
}
- long folderId = getFolderIdOrThrow(account, folderServerId);
+ long folderId = getFolderId(account, folderServerId);
for (MessagingListener messagingListener : getListeners(listener)) {
messagingListener.synchronizeMailboxFailed(account, folderId, message);
}
@@ -2825,7 +2793,7 @@ public class MessagingController {
@Override
public void folderStatusChanged(@NotNull String folderServerId) {
- long folderId = getFolderIdOrThrow(account, folderServerId);
+ long folderId = getFolderId(account, folderServerId);
for (MessagingListener messagingListener : getListeners(listener)) {
messagingListener.folderStatusChanged(account, folderId);
}
diff --git a/app/core/src/main/java/com/fsck/k9/controller/NotificationOperations.kt b/app/core/src/main/java/com/fsck/k9/controller/NotificationOperations.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8ab0a7f448536edeb47e6aa9185a5f6efc1ae89b
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/controller/NotificationOperations.kt
@@ -0,0 +1,63 @@
+package com.fsck.k9.controller
+
+import com.fsck.k9.Account
+import com.fsck.k9.Preferences
+import com.fsck.k9.mailstore.MessageStoreManager
+import com.fsck.k9.notification.NotificationController
+import com.fsck.k9.search.LocalSearch
+import com.fsck.k9.search.isNewMessages
+import com.fsck.k9.search.isSingleFolder
+import com.fsck.k9.search.isUnifiedInbox
+
+internal class NotificationOperations(
+ private val notificationController: NotificationController,
+ private val preferences: Preferences,
+ private val messageStoreManager: MessageStoreManager
+) {
+ fun clearNotifications(search: LocalSearch) {
+ if (search.isUnifiedInbox) {
+ clearUnifiedInboxNotifications()
+ } else if (search.isNewMessages) {
+ clearAllNotifications()
+ } else if (search.isSingleFolder) {
+ val account = search.firstAccount() ?: return
+ val folderId = search.folderIds.first()
+ clearNotifications(account, folderId)
+ } else {
+ // TODO: Remove notifications when updating the message list. That way we can easily remove only
+ // notifications for messages that are currently displayed in the list.
+ }
+ }
+
+ private fun clearUnifiedInboxNotifications() {
+ for (account in preferences.accounts) {
+ val messageStore = messageStoreManager.getMessageStore(account)
+
+ val folderIds = messageStore.getFolders(excludeLocalOnly = true) { folderDetails ->
+ if (folderDetails.isIntegrate) folderDetails.id else null
+ }.filterNotNull().toSet()
+
+ if (folderIds.isNotEmpty()) {
+ notificationController.clearNewMailNotifications(account) { messageReferences ->
+ messageReferences.filter { messageReference -> messageReference.folderId in folderIds }
+ }
+ }
+ }
+ }
+
+ private fun clearAllNotifications() {
+ for (account in preferences.accounts) {
+ notificationController.clearNewMailNotifications(account, clearNewMessageState = false)
+ }
+ }
+
+ private fun clearNotifications(account: Account, folderId: Long) {
+ notificationController.clearNewMailNotifications(account) { messageReferences ->
+ messageReferences.filter { messageReference -> messageReference.folderId == folderId }
+ }
+ }
+
+ private fun LocalSearch.firstAccount(): Account? {
+ return preferences.getAccount(accountUuids.first())
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/controller/NotificationState.kt b/app/core/src/main/java/com/fsck/k9/controller/NotificationState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..32306f5b25379b6df311dd7cdb8dd87967239b31
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/controller/NotificationState.kt
@@ -0,0 +1,6 @@
+package com.fsck.k9.controller
+
+class NotificationState {
+ @get:JvmName("wasNotified")
+ var wasNotified: Boolean = false
+}
diff --git a/app/core/src/main/java/com/fsck/k9/controller/ProgressBodyFactory.java b/app/core/src/main/java/com/fsck/k9/controller/ProgressBodyFactory.java
index 606541f27fa7baebc58e501affacd6ffa649dc2e..4fe052cd54f4165019236bff992a29907355778a 100644
--- a/app/core/src/main/java/com/fsck/k9/controller/ProgressBodyFactory.java
+++ b/app/core/src/main/java/com/fsck/k9/controller/ProgressBodyFactory.java
@@ -21,10 +21,8 @@ class ProgressBodyFactory extends DefaultBodyFactory {
@Override
protected void copyData(InputStream inputStream, OutputStream outputStream) throws IOException {
- final CountingOutputStream countingOutputStream = new CountingOutputStream(outputStream);
-
Timer timer = new Timer();
- try {
+ try (CountingOutputStream countingOutputStream = new CountingOutputStream(outputStream)) {
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
diff --git a/app/core/src/main/java/com/fsck/k9/controller/UnavailableAccountException.java b/app/core/src/main/java/com/fsck/k9/controller/UnavailableAccountException.java
deleted file mode 100644
index 55533828eb18d9cf82f73861f33b18458312dfae..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/controller/UnavailableAccountException.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.fsck.k9.controller;
-
-/**
- * An {@link com.fsck.k9.Account} is not
- * {@link com.fsck.k9.Account#isAvailable(android.content.Context)}.
- * The operation may be retried later.
- */
-public class UnavailableAccountException extends RuntimeException {
-
- /**
- *
- */
- private static final long serialVersionUID = -1827283277120501465L;
-
- public UnavailableAccountException() {
- super("please try again later");
- }
-
- /**
- * @param detailMessage
- * @param throwable
- */
- public UnavailableAccountException(String detailMessage, Throwable throwable) {
- super(detailMessage, throwable);
- }
-
- /**
- * @param detailMessage
- */
- public UnavailableAccountException(String detailMessage) {
- super(detailMessage);
- }
-
- /**
- * @param throwable
- */
- public UnavailableAccountException(Throwable throwable) {
- super(throwable);
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushController.kt b/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushController.kt
index a3df472cafbd4485b19c63fa21f88bb892280cdf..c735c20bc6787ac251b3db3b37785739cc8f3d39 100644
--- a/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushController.kt
+++ b/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushController.kt
@@ -1,11 +1,13 @@
package com.fsck.k9.controller.push
import com.fsck.k9.Account
+import com.fsck.k9.Account.FolderMode
+import com.fsck.k9.Preferences
import com.fsck.k9.backend.BackendManager
import com.fsck.k9.backend.api.BackendPusher
import com.fsck.k9.backend.api.BackendPusherCallback
import com.fsck.k9.controller.MessagingController
-import com.fsck.k9.mailstore.FolderRepositoryManager
+import com.fsck.k9.mailstore.FolderRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -17,11 +19,11 @@ import timber.log.Timber
internal class AccountPushController(
private val backendManager: BackendManager,
private val messagingController: MessagingController,
- folderRepositoryManager: FolderRepositoryManager,
+ private val preferences: Preferences,
+ private val folderRepository: FolderRepository,
backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val account: Account
) {
- private val folderRepository = folderRepositoryManager.getFolderRepository(account)
private val coroutineScope = CoroutineScope(backgroundDispatcher)
@Volatile
@@ -35,6 +37,11 @@ internal class AccountPushController(
override fun onPushError(exception: Exception) {
messagingController.handleException(account, exception)
}
+
+ override fun onPushNotSupported() {
+ Timber.v("AccountPushController(%s) - Push not supported. Disabling Push for account.", account.uuid)
+ disablePush()
+ }
}
fun start() {
@@ -68,7 +75,7 @@ internal class AccountPushController(
private fun startListeningForPushFolders() {
coroutineScope.launch {
- folderRepository.getPushFoldersFlow().collect { remoteFolders ->
+ folderRepository.getPushFoldersFlow(account).collect { remoteFolders ->
val folderServerIds = remoteFolders.map { it.serverId }
updatePushFolders(folderServerIds)
}
@@ -88,4 +95,9 @@ internal class AccountPushController(
private fun syncFolders(folderServerId: String) {
messagingController.synchronizeMailboxBlocking(account, folderServerId)
}
+
+ private fun disablePush() {
+ account.folderPushMode = FolderMode.NONE
+ preferences.saveAccount(account)
+ }
}
diff --git a/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushControllerFactory.kt b/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushControllerFactory.kt
index 9a083a45e8702cae48d057e1a99cea439442b5eb..7091496d69ae22a5b744dcc7443cc79cf806bfbf 100644
--- a/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushControllerFactory.kt
+++ b/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushControllerFactory.kt
@@ -1,16 +1,24 @@
package com.fsck.k9.controller.push
import com.fsck.k9.Account
+import com.fsck.k9.Preferences
import com.fsck.k9.backend.BackendManager
import com.fsck.k9.controller.MessagingController
-import com.fsck.k9.mailstore.FolderRepositoryManager
+import com.fsck.k9.mailstore.FolderRepository
internal class AccountPushControllerFactory(
private val backendManager: BackendManager,
private val messagingController: MessagingController,
- private val folderRepositoryManager: FolderRepositoryManager
+ private val folderRepository: FolderRepository,
+ private val preferences: Preferences
) {
fun create(account: Account): AccountPushController {
- return AccountPushController(backendManager, messagingController, folderRepositoryManager, account = account)
+ return AccountPushController(
+ backendManager,
+ messagingController,
+ preferences,
+ folderRepository,
+ account = account
+ )
}
}
diff --git a/app/core/src/main/java/com/fsck/k9/controller/push/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/controller/push/KoinModule.kt
index 146eec236a1ceef899f33416dfa5d2e06df47e94..b8046a970d17294f86a7eb44f5c583121a8a95d3 100644
--- a/app/core/src/main/java/com/fsck/k9/controller/push/KoinModule.kt
+++ b/app/core/src/main/java/com/fsck/k9/controller/push/KoinModule.kt
@@ -10,7 +10,8 @@ internal val controllerPushModule = module {
AccountPushControllerFactory(
backendManager = get(),
messagingController = get(),
- folderRepositoryManager = get()
+ folderRepository = get(),
+ preferences = get()
)
}
single {
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
new file mode 100644
index 0000000000000000000000000000000000000000..c04866115888f3788140c839bb129eba414e34a4
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/helper/CallbackFlowHelper.kt
@@ -0,0 +1,16 @@
+package com.fsck.k9.helper
+
+import kotlinx.coroutines.channels.ClosedSendChannelException
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.channels.sendBlocking
+
+/**
+ * Like [sendBlocking], but ignores [ClosedSendChannelException].
+ */
+fun SendChannel.sendBlockingSilently(element: E) {
+ try {
+ sendBlocking(element)
+ } catch (e: ClosedSendChannelException) {
+ // Ignore
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/helper/CursorExtensions.kt b/app/core/src/main/java/com/fsck/k9/helper/CursorExtensions.kt
index dcd348b7bf94471a95d4925f3b86f6429b35c5c2..61c60c7ba82299853f364239f771c23ab7462413 100644
--- a/app/core/src/main/java/com/fsck/k9/helper/CursorExtensions.kt
+++ b/app/core/src/main/java/com/fsck/k9/helper/CursorExtensions.kt
@@ -23,3 +23,15 @@ fun Cursor.getLongOrNull(columnName: String): Long? {
val columnIndex = getColumnIndex(columnName)
return if (isNull(columnIndex)) null else getLong(columnIndex)
}
+
+fun Cursor.getStringOrThrow(columnName: String): String {
+ return getStringOrNull(columnName) ?: error("Column $columnName must not be null")
+}
+
+fun Cursor.getIntOrThrow(columnName: String): Int {
+ return getIntOrNull(columnName) ?: error("Column $columnName must not be null")
+}
+
+fun Cursor.getLongOrThrow(columnName: String): Long {
+ return getLongOrNull(columnName) ?: error("Column $columnName must not be null")
+}
diff --git a/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.java b/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.java
index 5595d47f86db25c3363729ee009e70f057dcdc02..cb9e865dad8414b646010f883d4965780bbf6710 100644
--- a/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.java
+++ b/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.java
@@ -1,6 +1,8 @@
package com.fsck.k9.helper;
+import java.util.regex.Pattern;
+
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
@@ -27,6 +29,7 @@ public class MessageHelper {
* @see #toFriendly(Address[], com.fsck.k9.helper.Contacts)
*/
private static final int TOO_MANY_ADDRESSES = 50;
+ private static final Pattern SPOOF_ADDRESS_PATTERN = Pattern.compile("[^(]@");
private static MessageHelper sInstance;
@@ -143,6 +146,6 @@ public class MessageHelper {
}
private static boolean isSpoofAddress(String displayName) {
- return displayName.contains("@");
+ return displayName.contains("@") && SPOOF_ADDRESS_PATTERN.matcher(displayName).find();
}
}
diff --git a/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt b/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt
index d511c83cb6644398a0c9593940033e3fb5bd64fd..227909e9425b11d777ceaeba799ad6fa3ac791b3 100644
--- a/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt
+++ b/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt
@@ -23,7 +23,7 @@ class K9JobManager(
private fun scheduleMailSync() {
cancelAllMailSyncJobs()
- preferences.availableAccounts.forEach { account ->
+ preferences.accounts.forEach { account ->
mailSyncWorkerManager.scheduleMailSync(account)
}
}
diff --git a/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt
index 4bf3a7002f7e89663ed3ae854cd3a9ff82855ee0..4dd6f0e8fa3e15d5916db695765b0a70583fd8fa 100644
--- a/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt
+++ b/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt
@@ -1,7 +1,6 @@
package com.fsck.k9.job
import androidx.work.WorkerFactory
-import com.fsck.k9.Clock
import org.koin.dsl.module
val jobModule = module {
@@ -9,5 +8,5 @@ val jobModule = module {
single { K9WorkerFactory(get(), get()) }
single { get().getWorkManager() }
single { K9JobManager(get(), get(), get()) }
- factory { MailSyncWorkerManager(get(), Clock.INSTANCE) }
+ factory { MailSyncWorkerManager(workManager = get(), clock = get()) }
}
diff --git a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt
index bb1237c1038cb54a81c190fba88a91714cf7c349..193c24a5ccc6c2eb8d86b8ba6a47dd43bcd551a2 100644
--- a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt
+++ b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt
@@ -39,6 +39,11 @@ class MailSyncWorker(
return Result.success()
}
+ if (account.incomingServerSettings.isMissingCredentials) {
+ Timber.d("Password for this account is missing. Skipping mail sync.")
+ return Result.success()
+ }
+
val success = messagingController.performPeriodicMailSync(account)
return if (success) Result.success() else Result.retry()
diff --git a/app/core/src/main/java/com/fsck/k9/logging/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/logging/KoinModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4f0cf6048020b5116756408dac2be607dca44f12
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/logging/KoinModule.kt
@@ -0,0 +1,8 @@
+package com.fsck.k9.logging
+
+import org.koin.dsl.module
+
+val loggingModule = module {
+ factory { RealProcessExecutor() }
+ factory { LogcatLogFileWriter(contentResolver = get(), processExecutor = get()) }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/logging/LogFileWriter.kt b/app/core/src/main/java/com/fsck/k9/logging/LogFileWriter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d81c9e16611ab7b3507d66392440b0b572e1c67a
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/logging/LogFileWriter.kt
@@ -0,0 +1,38 @@
+package com.fsck.k9.logging
+
+import android.content.ContentResolver
+import android.net.Uri
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.apache.commons.io.IOUtils
+import timber.log.Timber
+
+interface LogFileWriter {
+ suspend fun writeLogTo(contentUri: Uri)
+}
+
+class LogcatLogFileWriter(
+ private val contentResolver: ContentResolver,
+ private val processExecutor: ProcessExecutor,
+ private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO
+) : LogFileWriter {
+ override suspend fun writeLogTo(contentUri: Uri) {
+ return withContext(coroutineDispatcher) {
+ writeLogBlocking(contentUri)
+ }
+ }
+
+ private fun writeLogBlocking(contentUri: Uri) {
+ Timber.v("Writing logcat output to content URI: %s", contentUri)
+
+ val outputStream = contentResolver.openOutputStream(contentUri)
+ ?: error("Error opening contentUri for writing")
+
+ outputStream.use {
+ processExecutor.exec("logcat -d").use { inputStream ->
+ IOUtils.copy(inputStream, outputStream)
+ }
+ }
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/logging/ProcessExecutor.kt b/app/core/src/main/java/com/fsck/k9/logging/ProcessExecutor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cfa2ff653fc425eacc0774815cfd76c9009dfb90
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/logging/ProcessExecutor.kt
@@ -0,0 +1,14 @@
+package com.fsck.k9.logging
+
+import java.io.InputStream
+
+interface ProcessExecutor {
+ fun exec(command: String): InputStream
+}
+
+class RealProcessExecutor : ProcessExecutor {
+ override fun exec(command: String): InputStream {
+ val process = Runtime.getRuntime().exec(command)
+ return process.inputStream
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/AutoExpandFolderBackendFoldersRefreshListener.kt b/app/core/src/main/java/com/fsck/k9/mailstore/AutoExpandFolderBackendFoldersRefreshListener.kt
index e7394c8b9afc9b4ec208f2666cfca434df792de4..f709798876940cd6947ca3af3df5d1d5a09e431f 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/AutoExpandFolderBackendFoldersRefreshListener.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/AutoExpandFolderBackendFoldersRefreshListener.kt
@@ -11,8 +11,11 @@ class AutoExpandFolderBackendFoldersRefreshListener(
private val account: Account,
private val folderRepository: FolderRepository
) : BackendFoldersRefreshListener {
+ private var isFirstSync = false
- override fun onBeforeFolderListRefresh() = Unit
+ override fun onBeforeFolderListRefresh() {
+ isFirstSync = account.inboxFolderId == null
+ }
override fun onAfterFolderListRefresh() {
checkAutoExpandFolder()
@@ -22,17 +25,25 @@ class AutoExpandFolderBackendFoldersRefreshListener(
}
private fun checkAutoExpandFolder() {
- val folderId = account.importedAutoExpandFolder?.let { folderRepository.getFolderId(it) }
- if (folderId != null) {
- account.autoExpandFolderId = folderId
+ account.importedAutoExpandFolder?.let { folderName ->
+ if (folderName.isEmpty()) {
+ account.autoExpandFolderId = null
+ } else {
+ val folderId = folderRepository.getFolderId(account, folderName)
+ account.autoExpandFolderId = folderId
+ }
return
}
account.autoExpandFolderId?.let { autoExpandFolderId ->
- if (!folderRepository.isFolderPresent(autoExpandFolderId)) {
+ if (!folderRepository.isFolderPresent(account, autoExpandFolderId)) {
account.autoExpandFolderId = null
}
}
+
+ if (isFirstSync && account.autoExpandFolderId == null) {
+ account.autoExpandFolderId = account.inboxFolderId
+ }
}
private fun removeImportedAutoExpandFolder() {
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/FolderMapper.kt b/app/core/src/main/java/com/fsck/k9/mailstore/FolderMapper.kt
index f283618e82d9f9413da0e1248f5724863fdf8667..8cea58d994c549149deb9dc1f7ec61a358124e58 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/FolderMapper.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/FolderMapper.kt
@@ -21,7 +21,9 @@ interface FolderDetailsAccessor {
val pushClass: FolderClass
val visibleLimit: Int
val moreMessages: MoreMessages
- val messageCount: Int
+ val lastChecked: Long?
+ val unreadMessageCount: Int
+ val starredMessageCount: Int
fun serverIdOrThrow(): String
}
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepository.kt
index d211dbb52c02f8206d3f5bbb7d488510e733d9c4..f40136d34cc92743470f5526862f172d02815d64 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepository.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepository.kt
@@ -2,6 +2,10 @@ package com.fsck.k9.mailstore
import com.fsck.k9.Account
import com.fsck.k9.Account.FolderMode
+import com.fsck.k9.DI
+import com.fsck.k9.controller.MessagingController
+import com.fsck.k9.controller.SimpleMessagingListener
+import com.fsck.k9.helper.sendBlockingSilently
import com.fsck.k9.mail.FolderClass
import com.fsck.k9.preferences.AccountManager
import kotlinx.coroutines.CoroutineDispatcher
@@ -16,14 +20,12 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
import com.fsck.k9.mail.FolderType as RemoteFolderType
@OptIn(ExperimentalCoroutinesApi::class)
class FolderRepository(
private val messageStoreManager: MessageStoreManager,
private val accountManager: AccountManager,
- private val account: Account,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
private val sortForDisplay =
@@ -33,7 +35,7 @@ class FolderRepository(
.thenByDescending { it.isInTopGroup }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.folder.name }
- fun getDisplayFolders(displayMode: FolderMode?): List {
+ fun getDisplayFolders(account: Account, displayMode: FolderMode?): List {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getDisplayFolders(
displayMode = displayMode ?: account.folderDisplayMode,
@@ -43,35 +45,77 @@ class FolderRepository(
folder = Folder(
id = folder.id,
name = folder.name,
- type = folderTypeOf(folder.id),
+ type = folderTypeOf(account, folder.id),
isLocalOnly = folder.isLocalOnly
),
isInTopGroup = folder.isInTopGroup,
- unreadCount = folder.messageCount
+ unreadMessageCount = folder.unreadMessageCount,
+ starredMessageCount = folder.starredMessageCount
)
}.sortedWith(sortForDisplay)
}
- fun getFolder(folderId: Long): Folder? {
+ fun getDisplayFoldersFlow(account: Account, displayMode: FolderMode): Flow> {
+ val messagingController = DI.get()
+ val messageStore = messageStoreManager.getMessageStore(account)
+
+ return callbackFlow {
+ send(getDisplayFolders(account, displayMode))
+
+ val folderStatusChangedListener = object : SimpleMessagingListener() {
+ override fun folderStatusChanged(statusChangedAccount: Account, folderId: Long) {
+ if (statusChangedAccount.uuid == account.uuid) {
+ sendBlockingSilently(getDisplayFolders(account, displayMode))
+ }
+ }
+ }
+ messagingController.addListener(folderStatusChangedListener)
+
+ val folderSettingsChangedListener = FolderSettingsChangedListener {
+ sendBlockingSilently(getDisplayFolders(account, displayMode))
+ }
+ messageStore.addFolderSettingsChangedListener(folderSettingsChangedListener)
+
+ awaitClose {
+ messagingController.removeListener(folderStatusChangedListener)
+ messageStore.removeFolderSettingsChangedListener(folderSettingsChangedListener)
+ }
+ }.buffer(capacity = Channel.CONFLATED)
+ .distinctUntilChanged()
+ .flowOn(ioDispatcher)
+ }
+
+ fun getDisplayFoldersFlow(account: Account): Flow> {
+ return accountManager.getAccountFlow(account.uuid)
+ .map { latestAccount ->
+ AccountContainer(latestAccount, latestAccount.folderDisplayMode)
+ }
+ .distinctUntilChanged()
+ .flatMapLatest { (account, folderDisplayMode) ->
+ getDisplayFoldersFlow(account, folderDisplayMode)
+ }
+ }
+
+ fun getFolder(account: Account, folderId: Long): Folder? {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolder(folderId) { folder ->
Folder(
id = folder.id,
name = folder.name,
- type = folderTypeOf(folder.id),
+ type = folderTypeOf(account, folder.id),
isLocalOnly = folder.isLocalOnly
)
}
}
- fun getFolderDetails(folderId: Long): FolderDetails? {
+ fun getFolderDetails(account: Account, folderId: Long): FolderDetails? {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolder(folderId) { folder ->
FolderDetails(
folder = Folder(
id = folder.id,
name = folder.name,
- type = folderTypeOf(folder.id),
+ type = folderTypeOf(account, folder.id),
isLocalOnly = folder.isLocalOnly
),
isInTopGroup = folder.isInTopGroup,
@@ -84,7 +128,7 @@ class FolderRepository(
}
}
- fun getRemoteFolders(): List {
+ fun getRemoteFolders(account: Account): List {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolders(excludeLocalOnly = true) { folder ->
RemoteFolder(
@@ -96,7 +140,7 @@ class FolderRepository(
}
}
- fun getRemoteFolderDetails(): List {
+ fun getRemoteFolderDetails(account: Account): List {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolders(excludeLocalOnly = true) { folder ->
RemoteFolderDetails(
@@ -116,22 +160,20 @@ class FolderRepository(
}
}
- fun getPushFoldersFlow(): Flow> {
+ fun getPushFoldersFlow(account: Account): Flow> {
return account.getFolderPushModeFlow()
.flatMapLatest { pushMode ->
- getPushFoldersFlow(pushMode)
+ getPushFoldersFlow(account, pushMode)
}
}
- private fun getPushFoldersFlow(folderMode: FolderMode): Flow> {
+ private fun getPushFoldersFlow(account: Account, folderMode: FolderMode): Flow> {
val messageStore = messageStoreManager.getMessageStore(account)
return callbackFlow {
- send(getPushFolders(folderMode))
+ send(getPushFolders(account, folderMode))
val listener = FolderSettingsChangedListener {
- launch {
- send(getPushFolders(folderMode))
- }
+ sendBlockingSilently(getPushFolders(account, folderMode))
}
messageStore.addFolderSettingsChangedListener(listener)
@@ -143,10 +185,10 @@ class FolderRepository(
.flowOn(ioDispatcher)
}
- private fun getPushFolders(folderMode: FolderMode): List {
+ private fun getPushFolders(account: Account, folderMode: FolderMode): List {
if (folderMode == FolderMode.NONE) return emptyList()
- return getRemoteFolderDetails()
+ return getRemoteFolderDetails(account)
.asSequence()
.filter { folderDetails ->
val pushClass = folderDetails.effectivePushClass
@@ -164,54 +206,54 @@ class FolderRepository(
.toList()
}
- fun getFolderServerId(folderId: Long): String? {
+ fun getFolderServerId(account: Account, folderId: Long): String? {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolder(folderId) { folder ->
folder.serverId
}
}
- fun getFolderId(folderServerId: String): Long? {
+ fun getFolderId(account: Account, folderServerId: String): Long? {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolderId(folderServerId)
}
- fun isFolderPresent(folderId: Long): Boolean {
+ fun isFolderPresent(account: Account, folderId: Long): Boolean {
val messageStore = messageStoreManager.getMessageStore(account)
return messageStore.getFolder(folderId) { true } ?: false
}
- fun updateFolderDetails(folderDetails: FolderDetails) {
+ fun updateFolderDetails(account: Account, folderDetails: FolderDetails) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.updateFolderSettings(folderDetails)
}
- fun setIncludeInUnifiedInbox(folderId: Long, includeInUnifiedInbox: Boolean) {
+ fun setIncludeInUnifiedInbox(account: Account, folderId: Long, includeInUnifiedInbox: Boolean) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.setIncludeInUnifiedInbox(folderId, includeInUnifiedInbox)
}
- fun setDisplayClass(folderId: Long, folderClass: FolderClass) {
+ fun setDisplayClass(account: Account, folderId: Long, folderClass: FolderClass) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.setDisplayClass(folderId, folderClass)
}
- fun setSyncClass(folderId: Long, folderClass: FolderClass) {
+ fun setSyncClass(account: Account, folderId: Long, folderClass: FolderClass) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.setSyncClass(folderId, folderClass)
}
- fun setPushClass(folderId: Long, folderClass: FolderClass) {
+ fun setPushClass(account: Account, folderId: Long, folderClass: FolderClass) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.setPushClass(folderId, folderClass)
}
- fun setNotificationClass(folderId: Long, folderClass: FolderClass) {
+ fun setNotificationClass(account: Account, folderId: Long, folderClass: FolderClass) {
val messageStore = messageStoreManager.getMessageStore(account)
messageStore.setNotificationClass(folderId, folderClass)
}
- private fun folderTypeOf(folderId: Long) = when (folderId) {
+ private fun folderTypeOf(account: Account, folderId: Long) = when (folderId) {
account.inboxFolderId -> FolderType.INBOX
account.outboxFolderId -> FolderType.OUTBOX
account.sentFolderId -> FolderType.SENT
@@ -244,6 +286,11 @@ class FolderRepository(
get() = if (syncClass == FolderClass.INHERITED) displayClass else syncClass
}
+private data class AccountContainer(
+ val account: Account,
+ val folderDisplayMode: FolderMode
+)
+
data class Folder(val id: Long, val name: String, val type: FolderType, val isLocalOnly: Boolean)
data class RemoteFolder(val id: Long, val serverId: String, val name: String, val type: FolderType)
@@ -271,7 +318,8 @@ data class RemoteFolderDetails(
data class DisplayFolder(
val folder: Folder,
val isInTopGroup: Boolean,
- val unreadCount: Int
+ val unreadMessageCount: Int,
+ val starredMessageCount: Int
)
enum class FolderType {
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepositoryManager.kt b/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepositoryManager.kt
deleted file mode 100644
index 95bae73c20f8f029c9011ac9c2de9d0262b69b8b..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/mailstore/FolderRepositoryManager.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.fsck.k9.mailstore
-
-import com.fsck.k9.Account
-import com.fsck.k9.preferences.AccountManager
-
-class FolderRepositoryManager(
- private val messageStoreManager: MessageStoreManager,
- private val accountManager: AccountManager
-) {
- fun getFolderRepository(account: Account): FolderRepository {
- return FolderRepository(messageStoreManager, accountManager, account)
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendFolder.kt b/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendFolder.kt
index e2f71c941d53ba5130608a63190474e0599b0382..c00daf0ea2c543e5a1950caa103a54aece94aafa 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendFolder.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendFolder.kt
@@ -35,10 +35,6 @@ class K9BackendFolder(
visibleLimit = init.visibleLimit
}
- override fun getLastUid(): Long? {
- return messageStore.getLastUid(folderId)
- }
-
override fun getMessageServerIds(): Set {
return messageStore.getMessageServerIds(folderId)
}
@@ -67,7 +63,7 @@ class K9BackendFolder(
}
override fun setLastChecked(timestamp: Long) {
- messageStore.setLastUpdated(folderId, timestamp)
+ messageStore.setLastChecked(folderId, timestamp)
}
override fun setStatus(status: String?) {
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorage.kt b/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorage.kt
index 5fc159cbab12bf7c4de63a68bdd3a88d9a8b93bc..566a13ca454e32939123b73d77559417c19ab0d2 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorage.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorage.kt
@@ -60,7 +60,9 @@ class K9BackendStorage(
}
override fun deleteFolders(folderServerIds: List) {
- messageStore.deleteFolders(folderServerIds)
+ if (folderServerIds.isNotEmpty()) {
+ messageStore.deleteFolders(folderServerIds)
+ }
}
override fun changeFolder(folderServerId: String, name: String, type: RemoteFolderType) {
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorageFactory.kt b/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorageFactory.kt
index ab42cf04a5c71991b20558dc9f42ffaca85193cf..211c74a57cfdbe5fb4316b6671cd93142b54a621 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorageFactory.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/K9BackendStorageFactory.kt
@@ -5,13 +5,12 @@ import com.fsck.k9.Preferences
class K9BackendStorageFactory(
private val preferences: Preferences,
- private val folderRepositoryManager: FolderRepositoryManager,
+ private val folderRepository: FolderRepository,
private val messageStoreManager: MessageStoreManager,
private val specialFolderSelectionStrategy: SpecialFolderSelectionStrategy,
private val saveMessageDataCreator: SaveMessageDataCreator
) {
fun createBackendStorage(account: Account): K9BackendStorage {
- val folderRepository = folderRepositoryManager.getFolderRepository(account)
val messageStore = messageStoreManager.getMessageStore(account)
val folderSettingsProvider = FolderSettingsProvider(preferences, account)
val specialFolderUpdater = SpecialFolderUpdater(
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt
index 3a14fb1bd19a856dcae042875d98bd66dd4d1a81..5f657ecaa410c21e0329eef956d8c7b95087df87 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt
@@ -6,7 +6,7 @@ import com.fsck.k9.message.extractors.MessagePreviewCreator
import org.koin.dsl.module
val mailStoreModule = module {
- single { FolderRepositoryManager(messageStoreManager = get(), accountManager = get()) }
+ single { FolderRepository(messageStoreManager = get(), accountManager = get()) }
single { MessageViewInfoExtractorFactory(get(), get(), get()) }
single { StorageManager.getInstance(get()) }
single { SearchStatusManager() }
@@ -14,7 +14,7 @@ val mailStoreModule = module {
single {
K9BackendStorageFactory(
preferences = get(),
- folderRepositoryManager = get(),
+ folderRepository = get(),
messageStoreManager = get(),
specialFolderSelectionStrategy = get(),
saveMessageDataCreator = get()
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 8ef74657a86921895f2105f01d1c80f598c84b04..02b508d7c403b6cb13783f1ae02974ba51ae00b7 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
@@ -33,7 +33,6 @@ import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.SizeAware;
import com.fsck.k9.mail.message.MessageHeaderParser;
import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
-import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
import com.fsck.k9.message.extractors.AttachmentInfoExtractor;
import org.apache.commons.io.IOUtils;
@@ -140,38 +139,32 @@ public class LocalFolder {
return;
}
- try {
- this.localStore.getDatabase().execute(false, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
- Cursor cursor = null;
- try {
- String baseQuery = "SELECT " + LocalStore.GET_FOLDER_COLS + " FROM folders ";
+ this.localStore.getDatabase().execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
+ Cursor cursor = null;
+ try {
+ String baseQuery = "SELECT " + LocalStore.GET_FOLDER_COLS + " FROM folders ";
- if (serverId != null) {
- cursor = db.rawQuery(baseQuery + "where folders.server_id = ?", new String[] { serverId });
- } else {
- cursor = db.rawQuery(baseQuery + "where folders.id = ?", new String[] { Long.toString(
- databaseId) });
- }
+ if (serverId != null) {
+ cursor = db.rawQuery(baseQuery + "where folders.server_id = ?", new String[] { serverId });
+ } else {
+ cursor = db.rawQuery(baseQuery + "where folders.id = ?", new String[] { Long.toString(
+ databaseId) });
+ }
- if (cursor.moveToFirst() && !cursor.isNull(LocalStore.FOLDER_ID_INDEX)) {
- open(cursor);
- } else {
- throw new MessagingException("LocalFolder.open(): Folder not found: " +
- serverId + " (" + databaseId + ")", true);
- }
- } catch (MessagingException e) {
- throw new WrappedException(e);
- } finally {
- Utility.closeQuietly(cursor);
+ if (cursor.moveToFirst() && !cursor.isNull(LocalStore.FOLDER_ID_INDEX)) {
+ open(cursor);
+ } else {
+ throw new MessagingException("LocalFolder.open(): Folder not found: " +
+ serverId + " (" + databaseId + ")", true);
}
- return null;
+ } finally {
+ Utility.closeQuietly(cursor);
}
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+ return null;
+ }
+ });
}
void open(Cursor cursor) throws MessagingException {
@@ -214,17 +207,13 @@ public class LocalFolder {
}
public void setName(String name) throws MessagingException {
- try {
- open();
-
- if (name.equals(this.name)) {
- return;
- }
+ open();
- this.name = name;
- } catch (MessagingException e) {
- throw new WrappedException(e);
+ if (name.equals(this.name)) {
+ return;
}
+
+ this.name = name;
updateFolderColumn("name", name);
}
@@ -240,7 +229,7 @@ public class LocalFolder {
public boolean exists() throws MessagingException {
return this.localStore.getDatabase().execute(false, new DbCallback() {
@Override
- public Boolean doDbWork(final SQLiteDatabase db) throws WrappedException {
+ public Boolean doDbWork(final SQLiteDatabase db) {
Cursor cursor = null;
try {
cursor = db.rawQuery("SELECT id FROM folders where id = ?",
@@ -258,72 +247,30 @@ public class LocalFolder {
});
}
- public int getMessageCount() throws MessagingException {
- try {
- return this.localStore.getDatabase().execute(false, new DbCallback() {
- @Override
- public Integer doDbWork(final SQLiteDatabase db) throws WrappedException {
- try {
- open();
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
- Cursor cursor = null;
- try {
- cursor = db.rawQuery(
- "SELECT COUNT(id) FROM messages " +
- "WHERE empty = 0 AND deleted = 0 and folder_id = ?",
- new String[] { Long.toString(databaseId) });
- cursor.moveToFirst();
- return cursor.getInt(0); //messagecount
- } finally {
- Utility.closeQuietly(cursor);
- }
- }
- });
- } catch (WrappedException e) {
- throw (MessagingException) e.getCause();
- }
- }
-
public int getUnreadMessageCount() throws MessagingException {
if (databaseId == -1L) {
open();
}
- try {
- return this.localStore.getDatabase().execute(false, new DbCallback() {
- @Override
- public Integer doDbWork(final SQLiteDatabase db) throws WrappedException {
- int unreadMessageCount = 0;
- Cursor cursor = db.query("messages", new String[] { "COUNT(id)" },
- "folder_id = ? AND empty = 0 AND deleted = 0 AND read=0",
- new String[] { Long.toString(databaseId) }, null, null, null);
+ return this.localStore.getDatabase().execute(false, new DbCallback() {
+ @Override
+ public Integer doDbWork(final SQLiteDatabase db) {
+ int unreadMessageCount = 0;
+ Cursor cursor = db.query("messages", new String[] { "COUNT(id)" },
+ "folder_id = ? AND empty = 0 AND deleted = 0 AND read=0",
+ new String[] { Long.toString(databaseId) }, null, null, null);
- try {
- if (cursor.moveToFirst()) {
- unreadMessageCount = cursor.getInt(0);
- }
- } finally {
- cursor.close();
+ try {
+ if (cursor.moveToFirst()) {
+ unreadMessageCount = cursor.getInt(0);
}
-
- return unreadMessageCount;
+ } finally {
+ cursor.close();
}
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
- }
- public void setLastChecked(final long lastChecked) throws MessagingException {
- try {
- open();
- this.lastChecked = lastChecked;
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
- updateFolderColumn("last_updated", lastChecked);
+ return unreadMessageCount;
+ }
+ });
}
public int getVisibleLimit() throws MessagingException {
@@ -356,22 +303,14 @@ public class LocalFolder {
}
private void updateFolderColumn(final String column, final Object value) throws MessagingException {
- try {
- this.localStore.getDatabase().execute(false, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
- try {
- open();
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
- db.execSQL("UPDATE folders SET " + column + " = ? WHERE id = ?", new Object[] { value, databaseId });
- return null;
- }
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+ this.localStore.getDatabase().execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
+ open();
+ db.execSQL("UPDATE folders SET " + column + " = ? WHERE id = ?", new Object[] { value, databaseId });
+ return null;
+ }
+ });
}
public FolderClass getDisplayClass() {
@@ -382,26 +321,14 @@ public class LocalFolder {
return (FolderClass.INHERITED == syncClass) ? getDisplayClass() : syncClass;
}
- public FolderClass getRawSyncClass() {
- return syncClass;
- }
-
public FolderClass getNotifyClass() {
return (FolderClass.INHERITED == notifyClass) ? getPushClass() : notifyClass;
}
- public FolderClass getRawNotifyClass() {
- return notifyClass;
- }
-
public FolderClass getPushClass() {
return (FolderClass.INHERITED == pushClass) ? getSyncClass() : pushClass;
}
- public FolderClass getRawPushClass() {
- return pushClass;
- }
-
public void setDisplayClass(FolderClass displayClass) throws MessagingException {
this.displayClass = displayClass;
updateFolderColumn("display_class", this.displayClass.name());
@@ -450,26 +377,18 @@ public class LocalFolder {
public void fetch(final List messages, final FetchProfile fp, final MessageRetrievalListener listener)
throws MessagingException {
- try {
- this.localStore.getDatabase().execute(false, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
- try {
- open();
- if (fp.contains(FetchProfile.Item.BODY)) {
- for (LocalMessage message : messages) {
- loadMessageParts(db, message);
- }
- }
- } catch (MessagingException e) {
- throw new WrappedException(e);
+ this.localStore.getDatabase().execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
+ open();
+ if (fp.contains(FetchProfile.Item.BODY)) {
+ for (LocalMessage message : messages) {
+ loadMessageParts(db, message);
}
- return null;
}
- });
- } catch (WrappedException e) {
- throw (MessagingException) e.getCause();
- }
+ return null;
+ }
+ });
}
private void loadMessageParts(SQLiteDatabase db, LocalMessage message) throws MessagingException {
@@ -578,71 +497,55 @@ public class LocalFolder {
}
public String getMessageUidById(final long id) throws MessagingException {
- try {
- return this.localStore.getDatabase().execute(false, new DbCallback() {
- @Override
- public String doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
- try {
- open();
- Cursor cursor = null;
-
- try {
- cursor = db.rawQuery(
- "SELECT uid FROM messages WHERE id = ? AND folder_id = ?",
- new String[] { Long.toString(id), Long.toString(LocalFolder.this.databaseId) });
- if (!cursor.moveToNext()) {
- return null;
- }
- return cursor.getString(0);
- } finally {
- Utility.closeQuietly(cursor);
- }
- } catch (MessagingException e) {
- throw new WrappedException(e);
+ return this.localStore.getDatabase().execute(false, new DbCallback() {
+ @Override
+ public String doDbWork(final SQLiteDatabase db) throws MessagingException {
+ open();
+ Cursor cursor = null;
+
+ try {
+ cursor = db.rawQuery(
+ "SELECT uid FROM messages WHERE id = ? AND folder_id = ?",
+ new String[] { Long.toString(id), Long.toString(LocalFolder.this.databaseId) });
+ if (!cursor.moveToNext()) {
+ return null;
}
+ return cursor.getString(0);
+ } finally {
+ Utility.closeQuietly(cursor);
}
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+ }
+ });
}
public LocalMessage getMessage(final String uid) throws MessagingException {
- try {
- return this.localStore.getDatabase().execute(false, new DbCallback() {
- @Override
- public LocalMessage doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
- try {
- open();
- LocalMessage message = new LocalMessage(LocalFolder.this.localStore, uid, LocalFolder.this);
- Cursor cursor = null;
-
- try {
- cursor = db.rawQuery(
- "SELECT " +
- LocalStore.GET_MESSAGES_COLS +
- "FROM messages " +
- "LEFT JOIN message_parts ON (message_parts.id = messages.message_part_id) " +
- "LEFT JOIN threads ON (threads.message_id = messages.id) " +
- "WHERE uid = ? AND folder_id = ?",
- new String[] { message.getUid(), Long.toString(databaseId) });
-
- if (!cursor.moveToNext()) {
- return null;
- }
- message.populateFromGetMessageCursor(cursor);
- } finally {
- Utility.closeQuietly(cursor);
- }
- return message;
- } catch (MessagingException e) {
- throw new WrappedException(e);
+ return this.localStore.getDatabase().execute(false, new DbCallback() {
+ @Override
+ public LocalMessage doDbWork(final SQLiteDatabase db) throws MessagingException {
+ open();
+ LocalMessage message = new LocalMessage(LocalFolder.this.localStore, uid, LocalFolder.this);
+ Cursor cursor = null;
+
+ try {
+ cursor = db.rawQuery(
+ "SELECT " +
+ LocalStore.GET_MESSAGES_COLS +
+ "FROM messages " +
+ "LEFT JOIN message_parts ON (message_parts.id = messages.message_part_id) " +
+ "LEFT JOIN threads ON (threads.message_id = messages.id) " +
+ "WHERE uid = ? AND folder_id = ?",
+ new String[] { message.getUid(), Long.toString(databaseId) });
+
+ if (!cursor.moveToNext()) {
+ return null;
}
+ message.populateFromGetMessageCursor(cursor);
+ } finally {
+ Utility.closeQuietly(cursor);
}
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+ return message;
+ }
+ });
}
@Nullable
@@ -679,65 +582,21 @@ public class LocalFolder {
public List getMessages(final MessageRetrievalListener listener,
final boolean includeDeleted) throws MessagingException {
- try {
- return localStore.getDatabase().execute(false, new DbCallback>() {
- @Override
- public List doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
- try {
- open();
- return LocalFolder.this.localStore.getMessages(listener, LocalFolder.this,
- "SELECT " + LocalStore.GET_MESSAGES_COLS +
- "FROM messages " +
- "LEFT JOIN message_parts ON (message_parts.id = messages.message_part_id) " +
- "LEFT JOIN threads ON (threads.message_id = messages.id) " +
- "WHERE empty = 0 AND " +
- (includeDeleted ? "" : "deleted = 0 AND ") +
- "folder_id = ? ORDER BY date DESC",
- new String[] { Long.toString(databaseId) });
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
- }
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
- }
-
- public List getAllMessageUids() throws MessagingException {
- try {
- return localStore.getDatabase().execute(false, new DbCallback>() {
- @Override
- public List doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
- Cursor cursor = null;
- ArrayList result = new ArrayList<>();
-
- try {
- open();
-
- cursor = db.rawQuery(
- "SELECT uid " +
- "FROM messages " +
- "WHERE empty = 0 AND deleted = 0 AND " +
- "folder_id = ? ORDER BY date DESC",
- new String[] { Long.toString(databaseId) });
-
- while (cursor.moveToNext()) {
- String uid = cursor.getString(0);
- result.add(uid);
- }
- } catch (MessagingException e) {
- throw new WrappedException(e);
- } finally {
- Utility.closeQuietly(cursor);
- }
-
- return result;
- }
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+ 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,
+ "SELECT " + LocalStore.GET_MESSAGES_COLS +
+ "FROM messages " +
+ "LEFT JOIN message_parts ON (message_parts.id = messages.message_part_id) " +
+ "LEFT JOIN threads ON (threads.message_id = messages.id) " +
+ "WHERE empty = 0 AND " +
+ (includeDeleted ? "" : "deleted = 0 AND ") +
+ "folder_id = ? ORDER BY date DESC",
+ new String[] { Long.toString(databaseId) });
+ }
+ });
}
public List getMessagesByUids(@NonNull List uids) throws MessagingException {
@@ -776,24 +635,16 @@ public class LocalFolder {
return messages;
}
- public void destroyMessages(final List messages) {
- try {
- this.localStore.getDatabase().execute(true, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
- for (LocalMessage message : messages) {
- try {
- message.destroy();
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
- }
- return null;
+ public void destroyMessages(final List messages) throws MessagingException {
+ this.localStore.getDatabase().execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
+ for (LocalMessage message : messages) {
+ message.destroy();
}
- });
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
+ return null;
+ }
+ });
}
private void moveTemporaryFile(File tempFile, String messagePartId) throws IOException {
@@ -934,20 +785,15 @@ public class LocalFolder {
}
private long decodeAndCountBytes(InputStream rawInputStream, String encoding, long fallbackValue) {
- InputStream decodingInputStream = localStore.getDecodingInputStream(rawInputStream, encoding);
- try {
- CountingOutputStream countingOutputStream = new CountingOutputStream();
- try {
- IOUtils.copy(decodingInputStream, countingOutputStream);
+ try (InputStream decodingInputStream = localStore.getDecodingInputStream(rawInputStream, encoding)) {
+ try (CountingOutputStream countingOutputStream = new CountingOutputStream()) {
+ IOUtils.copy(decodingInputStream, countingOutputStream);
return countingOutputStream.getCount();
- } catch (IOException e) {
- return fallbackValue;
}
- } finally {
- try {
- decodingInputStream.close();
- } catch (IOException e) { /* ignore */ }
+
+ } catch (IOException e) {
+ return fallbackValue;
}
}
@@ -977,7 +823,7 @@ public class LocalFolder {
localStore.getDatabase().execute(false, new DbCallback() {
@Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ public Void doDbWork(final SQLiteDatabase db) {
long messagePartId;
Cursor cursor = db.query("message_parts", new String[] { "id" }, "root = ? AND server_extra = ?",
@@ -1016,7 +862,7 @@ public class LocalFolder {
cv.put("uid", message.getUid());
this.localStore.getDatabase().execute(false, new DbCallback() {
@Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ public Void doDbWork(final SQLiteDatabase db) {
db.update("messages", cv, "id = ?", new String[]
{ Long.toString(message.getDatabaseId()) });
return null;
@@ -1032,26 +878,21 @@ public class LocalFolder {
open();
// Use one transaction to set all flags
- try {
- this.localStore.getDatabase().execute(true, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
- UnavailableStorageException {
+ this.localStore.getDatabase().execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) {
- for (LocalMessage message : messages) {
- try {
- message.setFlags(flags, value);
- } catch (MessagingException e) {
- Timber.e(e, "Something went wrong while setting flag");
- }
+ for (LocalMessage message : messages) {
+ try {
+ message.setFlags(flags, value);
+ } catch (MessagingException e) {
+ Timber.e(e, "Something went wrong while setting flag");
}
-
- return null;
}
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+
+ return null;
+ }
+ });
}
public void setFlags(final Set flags, boolean value)
@@ -1067,45 +908,45 @@ public class LocalFolder {
open();
- try {
- this.localStore.getDatabase().execute(false, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
- try {
- Cursor cursor = db.query("messages", new String[] { "message_part_id" },
- "folder_id = ? AND empty = 0",
- folderIdArg, null, null, null);
- try {
- while (cursor.moveToNext()) {
- long messagePartId = cursor.getLong(0);
- deleteMessageDataFromDisk(messagePartId);
- }
- } finally {
- cursor.close();
- }
+ this.localStore.getDatabase().execute(false, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
+ Cursor cursor = db.query("messages", new String[] { "message_part_id" },
+ "folder_id = ? AND empty = 0",
+ folderIdArg, null, null, null);
+ try {
+ while (cursor.moveToNext()) {
+ long messagePartId = cursor.getLong(0);
+ deleteMessageDataFromDisk(messagePartId);
+ }
+ } finally {
+ cursor.close();
+ }
- db.execSQL("DELETE FROM threads WHERE message_id IN " +
- "(SELECT id FROM messages WHERE folder_id = ?)", folderIdArg);
- db.execSQL("DELETE FROM messages WHERE folder_id = ?", folderIdArg);
+ db.execSQL("DELETE FROM threads WHERE message_id IN " +
+ "(SELECT id FROM messages WHERE folder_id = ?)", folderIdArg);
+ db.execSQL("DELETE FROM messages WHERE folder_id = ?", folderIdArg);
- setMoreMessages(MoreMessages.UNKNOWN);
+ setMoreMessages(MoreMessages.UNKNOWN);
+ resetLastChecked(db);
- return null;
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
- }
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+ return null;
+ }
+ });
this.localStore.notifyChange();
- setLastChecked(0);
setVisibleLimit(getAccount().getDisplayCount());
}
+ private void resetLastChecked(SQLiteDatabase db) {
+ lastChecked = 0;
+
+ ContentValues values = new ContentValues();
+ values.putNull("last_updated");
+ db.update("folders", values, "id = ?", new String[] { Long.toString(databaseId) });
+ }
+
public void destroyLocalOnlyMessages() throws MessagingException {
destroyMessages("uid LIKE '" + K9.LOCAL_UID_PREFIX + "%'");
}
@@ -1159,66 +1000,57 @@ public class LocalFolder {
private void destroyMessage(final long messageId, final long messagePartId, final String messageIdHeader)
throws MessagingException {
- try {
- localStore.getDatabase().execute(true, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
- UnavailableStorageException {
- try {
- deleteMessagePartsAndDataFromDisk(messagePartId);
+ localStore.getDatabase().execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
+ deleteMessagePartsAndDataFromDisk(messagePartId);
- deleteFulltextIndexEntry(db, messageId);
+ deleteFulltextIndexEntry(db, messageId);
- if (hasThreadChildren(db, messageId)) {
- // This message has children in the thread structure so we need to
- // make it an empty message.
- ContentValues cv = new ContentValues();
- cv.put("id", messageId);
- cv.put("folder_id", getDatabaseId());
- cv.put("deleted", 0);
- cv.put("message_id", messageIdHeader);
- cv.put("empty", 1);
+ if (hasThreadChildren(db, messageId)) {
+ // This message has children in the thread structure so we need to
+ // make it an empty message.
+ ContentValues cv = new ContentValues();
+ cv.put("id", messageId);
+ cv.put("folder_id", getDatabaseId());
+ cv.put("deleted", 0);
+ cv.put("message_id", messageIdHeader);
+ cv.put("empty", 1);
- db.replace("messages", null, cv);
+ db.replace("messages", null, cv);
- // Nothing else to do
- return null;
- }
+ // Nothing else to do
+ return null;
+ }
- // Get the message ID of the parent message if it's empty
- long currentId = getEmptyThreadParent(db, messageId);
+ // Get the message ID of the parent message if it's empty
+ long currentId = getEmptyThreadParent(db, messageId);
- // Delete the placeholder message
- deleteMessageRow(db, messageId);
+ // Delete the placeholder message
+ deleteMessageRow(db, messageId);
- /*
- * Walk the thread tree to delete all empty parents without children
- */
+ /*
+ * Walk the thread tree to delete all empty parents without children
+ */
- while (currentId != -1) {
- if (hasThreadChildren(db, currentId)) {
- // We made sure there are no empty leaf nodes and can stop now.
- break;
- }
+ while (currentId != -1) {
+ if (hasThreadChildren(db, currentId)) {
+ // We made sure there are no empty leaf nodes and can stop now.
+ break;
+ }
- // Get ID of the (empty) parent for the next iteration
- long newId = getEmptyThreadParent(db, currentId);
+ // Get ID of the (empty) parent for the next iteration
+ long newId = getEmptyThreadParent(db, currentId);
- // Delete the empty message
- deleteMessageRow(db, currentId);
+ // Delete the empty message
+ deleteMessageRow(db, currentId);
- currentId = newId;
- }
-
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
- return null;
+ currentId = newId;
}
- });
- } catch (WrappedException e) {
- throw (MessagingException) e.getCause();
- }
+
+ return null;
+ }
+ });
localStore.notifyChange();
}
@@ -1296,7 +1128,7 @@ public class LocalFolder {
private void deleteMessageParts(final long rootMessagePartId) throws MessagingException {
localStore.getDatabase().execute(false, new DbCallback() {
@Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ public Void doDbWork(final SQLiteDatabase db) {
db.delete("message_parts", "root = ?", new String[] { Long.toString(rootMessagePartId) });
return null;
}
@@ -1306,7 +1138,7 @@ public class LocalFolder {
private void deleteMessageDataFromDisk(final long rootMessagePartId) throws MessagingException {
localStore.getDatabase().execute(false, new DbCallback() {
@Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+ public Void doDbWork(final SQLiteDatabase db) {
deleteMessagePartsFromDisk(db, rootMessagePartId);
return null;
}
@@ -1344,73 +1176,65 @@ public class LocalFolder {
public List extractNewMessages(final List messageServerIds)
throws MessagingException {
- try {
- return this.localStore.getDatabase().execute(false, new DbCallback>() {
- @Override
- public List doDbWork(final SQLiteDatabase db) throws WrappedException {
- try {
- open();
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
-
- List result = new ArrayList<>();
+ return this.localStore.getDatabase().execute(false, new DbCallback>() {
+ @Override
+ public List doDbWork(final SQLiteDatabase db) throws MessagingException {
+ open();
- List selectionArgs = new ArrayList<>();
- Set existingMessages = new HashSet<>();
- int start = 0;
+ List result = new ArrayList<>();
- while (start < messageServerIds.size()) {
- StringBuilder selection = new StringBuilder();
+ List selectionArgs = new ArrayList<>();
+ Set existingMessages = new HashSet<>();
+ int start = 0;
- selection.append("folder_id = ? AND UID IN (");
- selectionArgs.add(Long.toString(databaseId));
+ while (start < messageServerIds.size()) {
+ StringBuilder selection = new StringBuilder();
- int count = Math.min(messageServerIds.size() - start, LocalStore.UID_CHECK_BATCH_SIZE);
+ selection.append("folder_id = ? AND UID IN (");
+ selectionArgs.add(Long.toString(databaseId));
- for (int i = start, end = start + count; i < end; i++) {
- if (i > start) {
- selection.append(",?");
- } else {
- selection.append("?");
- }
+ int count = Math.min(messageServerIds.size() - start, LocalStore.UID_CHECK_BATCH_SIZE);
- selectionArgs.add(messageServerIds.get(i));
+ for (int i = start, end = start + count; i < end; i++) {
+ if (i > start) {
+ selection.append(",?");
+ } else {
+ selection.append("?");
}
- selection.append(")");
+ selectionArgs.add(messageServerIds.get(i));
+ }
- Cursor cursor = db.query("messages", LocalStore.UID_CHECK_PROJECTION,
- selection.toString(), selectionArgs.toArray(LocalStore.EMPTY_STRING_ARRAY),
- null, null, null);
+ selection.append(")");
- try {
- while (cursor.moveToNext()) {
- String uid = cursor.getString(0);
- existingMessages.add(uid);
- }
- } finally {
- Utility.closeQuietly(cursor);
- }
+ Cursor cursor = db.query("messages", LocalStore.UID_CHECK_PROJECTION,
+ selection.toString(), selectionArgs.toArray(LocalStore.EMPTY_STRING_ARRAY),
+ null, null, null);
- for (int i = start, end = start + count; i < end; i++) {
- String messageServerId = messageServerIds.get(i);
- if (!existingMessages.contains(messageServerId)) {
- result.add(messageServerId);
- }
+ try {
+ while (cursor.moveToNext()) {
+ String uid = cursor.getString(0);
+ existingMessages.add(uid);
}
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
- existingMessages.clear();
- selectionArgs.clear();
- start += count;
+ for (int i = start, end = start + count; i < end; i++) {
+ String messageServerId = messageServerIds.get(i);
+ if (!existingMessages.contains(messageServerId)) {
+ result.add(messageServerId);
+ }
}
- return result;
+ existingMessages.clear();
+ selectionArgs.clear();
+ start += count;
}
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+
+ return result;
+ }
+ });
}
private Account getAccount() {
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/app/core/src/main/java/com/fsck/k9/mailstore/LocalMessage.java
index 51576753675ebed27fe788b64faa1a7d6a0882bd..c3d0de6613b61d26aaf01e263ca96205f5d0dddf 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/LocalMessage.java
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/LocalMessage.java
@@ -23,7 +23,6 @@ import com.fsck.k9.mail.internet.AddressHeaderBuilder;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.message.MessageHeaderParser;
import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
-import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
import com.fsck.k9.message.extractors.PreviewResult.PreviewType;
import timber.log.Timber;
@@ -238,66 +237,50 @@ public class LocalMessage extends MimeMessage {
}
public void setCachedDecryptedSubject(final String decryptedSubject) throws MessagingException {
- try {
- this.localStore.getDatabase().execute(true, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
- try {
- LocalMessage.super.setFlag(Flag.X_SUBJECT_DECRYPTED, true);
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
+ this.localStore.getDatabase().execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
+ LocalMessage.super.setFlag(Flag.X_SUBJECT_DECRYPTED, true);
- ContentValues cv = new ContentValues();
- cv.put("subject", decryptedSubject);
- cv.put("flags", LocalStore.serializeFlags(getFlags()));
+ ContentValues cv = new ContentValues();
+ cv.put("subject", decryptedSubject);
+ cv.put("flags", LocalStore.serializeFlags(getFlags()));
- db.update("messages", cv, "id = ?", new String[] { Long.toString(databaseId) });
+ db.update("messages", cv, "id = ?", new String[] { Long.toString(databaseId) });
- return null;
- }
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+ return null;
+ }
+ });
this.localStore.notifyChange();
}
@Override
public void setFlag(final Flag flag, final boolean set) throws MessagingException {
-
- try {
- this.localStore.getDatabase().execute(true, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
- try {
- if (flag == Flag.DELETED && set) {
- delete();
- }
-
- LocalMessage.super.setFlag(flag, set);
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
- /*
- * Set the flags on the message.
- */
- ContentValues cv = new ContentValues();
- cv.put("flags", LocalStore.serializeFlags(getFlags()));
- cv.put("read", isSet(Flag.SEEN) ? 1 : 0);
- cv.put("flagged", isSet(Flag.FLAGGED) ? 1 : 0);
- cv.put("answered", isSet(Flag.ANSWERED) ? 1 : 0);
- cv.put("forwarded", isSet(Flag.FORWARDED) ? 1 : 0);
-
- db.update("messages", cv, "id = ?", new String[] { Long.toString(databaseId) });
-
- return null;
+ this.localStore.getDatabase().execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
+ if (flag == Flag.DELETED && set) {
+ delete();
}
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+
+ LocalMessage.super.setFlag(flag, set);
+
+ /*
+ * Set the flags on the message.
+ */
+ ContentValues cv = new ContentValues();
+ cv.put("flags", LocalStore.serializeFlags(getFlags()));
+ cv.put("read", isSet(Flag.SEEN) ? 1 : 0);
+ cv.put("flagged", isSet(Flag.FLAGGED) ? 1 : 0);
+ cv.put("answered", isSet(Flag.ANSWERED) ? 1 : 0);
+ cv.put("forwarded", isSet(Flag.FORWARDED) ? 1 : 0);
+
+ db.update("messages", cv, "id = ?", new String[] { Long.toString(databaseId) });
+
+ return null;
+ }
+ });
this.localStore.notifyChange();
}
@@ -307,48 +290,40 @@ public class LocalMessage extends MimeMessage {
* row since we need to retain the UID for synchronization purposes.
*/
public void delete() throws MessagingException {
- try {
- localStore.getDatabase().execute(true, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
- ContentValues cv = new ContentValues();
- cv.put("deleted", 1);
- cv.put("preview_type", DatabasePreviewType.fromPreviewType(PreviewType.NONE).getDatabaseValue());
- cv.put("read", 0);
- cv.put("flagged", 0);
- cv.put("answered", 0);
- cv.put("forwarded", 0);
- cv.putNull("subject");
- cv.putNull("sender_list");
- cv.putNull("date");
- cv.putNull("to_list");
- cv.putNull("cc_list");
- cv.putNull("bcc_list");
- cv.putNull("preview");
- cv.putNull("reply_to_list");
- cv.putNull("message_part_id");
- cv.putNull("flags");
- cv.putNull("attachment_count");
- cv.putNull("internal_date");
- cv.putNull("mime_type");
- cv.putNull("encryption_type");
-
- db.update("messages", cv, "id = ?", new String[] { Long.toString(databaseId) });
-
- try {
- ((LocalFolder) mFolder).deleteMessagePartsAndDataFromDisk(messagePartId);
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
-
- getFolder().deleteFulltextIndexEntry(db, databaseId);
-
- return null;
- }
- });
- } catch (WrappedException e) {
- throw (MessagingException) e.getCause();
- }
+ localStore.getDatabase().execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
+ ContentValues cv = new ContentValues();
+ cv.put("deleted", 1);
+ cv.put("preview_type", DatabasePreviewType.fromPreviewType(PreviewType.NONE).getDatabaseValue());
+ cv.put("read", 0);
+ cv.put("flagged", 0);
+ cv.put("answered", 0);
+ cv.put("forwarded", 0);
+ cv.putNull("subject");
+ cv.putNull("sender_list");
+ cv.putNull("date");
+ cv.putNull("to_list");
+ cv.putNull("cc_list");
+ cv.putNull("bcc_list");
+ cv.putNull("preview");
+ cv.putNull("reply_to_list");
+ cv.putNull("message_part_id");
+ cv.putNull("flags");
+ cv.putNull("attachment_count");
+ cv.putNull("internal_date");
+ cv.putNull("mime_type");
+ cv.putNull("encryption_type");
+
+ db.update("messages", cv, "id = ?", new String[] { Long.toString(databaseId) });
+
+ mFolder.deleteMessagePartsAndDataFromDisk(messagePartId);
+
+ getFolder().deleteFulltextIndexEntry(db, databaseId);
+
+ return null;
+ }
+ });
localStore.notifyChange();
}
@@ -358,30 +333,22 @@ public class LocalMessage extends MimeMessage {
throw new AssertionError("method must only be used in developer mode!");
}
- try {
- localStore.getDatabase().execute(true, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException, MessagingException {
- ContentValues cv = new ContentValues();
- cv.putNull("message_part_id");
+ localStore.getDatabase().execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
+ ContentValues cv = new ContentValues();
+ cv.putNull("message_part_id");
- db.update("messages", cv, "id = ?", new String[] { Long.toString(databaseId) });
+ db.update("messages", cv, "id = ?", new String[] { Long.toString(databaseId) });
- try {
- ((LocalFolder) mFolder).deleteMessagePartsAndDataFromDisk(messagePartId);
- } catch (MessagingException e) {
- throw new WrappedException(e);
- }
+ mFolder.deleteMessagePartsAndDataFromDisk(messagePartId);
- setFlag(Flag.X_DOWNLOADED_FULL, false);
- setFlag(Flag.X_DOWNLOADED_PARTIAL, false);
+ setFlag(Flag.X_DOWNLOADED_FULL, false);
+ setFlag(Flag.X_DOWNLOADED_PARTIAL, false);
- return null;
- }
- });
- } catch (WrappedException e) {
- throw (MessagingException) e.getCause();
- }
+ return null;
+ }
+ });
localStore.notifyChange();
}
@@ -412,7 +379,7 @@ public class LocalMessage extends MimeMessage {
if (messageReference == null) {
String accountUuid = getFolder().getAccountUuid();
long folderId = getFolder().getDatabaseId();
- messageReference = new MessageReference(accountUuid, folderId, mUid, null);
+ messageReference = new MessageReference(accountUuid, folderId, mUid);
}
return messageReference;
}
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 01c8dbd6684b24915e95b6e308584c25b5dc5b23..773e2926eb6a8b062d9513927862d4d548e66c26 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
@@ -27,10 +27,11 @@ import android.net.Uri;
import androidx.annotation.Nullable;
import android.text.TextUtils;
+import androidx.core.database.CursorKt;
import com.fsck.k9.Account;
import com.fsck.k9.Clock;
import com.fsck.k9.DI;
-import com.fsck.k9.K9;
+import com.fsck.k9.controller.MessageCounts;
import com.fsck.k9.Preferences;
import com.fsck.k9.controller.MessagingControllerCommands.PendingCommand;
import com.fsck.k9.controller.PendingCommandSerializer;
@@ -49,13 +50,8 @@ import com.fsck.k9.mail.Part;
import com.fsck.k9.mailstore.LocalFolder.DataLocation;
import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
import com.fsck.k9.mailstore.LockableDatabase.SchemaDefinition;
-import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
import com.fsck.k9.mailstore.StorageManager.InternalStorageProvider;
-import com.fsck.k9.mailstore.StorageManager.StorageProvider;
-import com.fsck.k9.message.extractors.AttachmentCounter;
import com.fsck.k9.message.extractors.AttachmentInfoExtractor;
-import com.fsck.k9.message.extractors.MessageFulltextCreator;
-import com.fsck.k9.message.extractors.MessagePreviewCreator;
import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.provider.EmailProvider.MessageColumns;
import com.fsck.k9.search.LocalSearch;
@@ -115,6 +111,9 @@ public class LocalStore {
static final int MSG_INDEX_PREVIEW_TYPE = 24;
static final int MSG_INDEX_HEADER_DATA = 25;
+ static final int MSG_INDEX_NOTIFICATION_ID = 26;
+ static final int MSG_INDEX_NOTIFICATION_TIMESTAMP = 27;
+
static final String GET_FOLDER_COLS =
"folders.id, name, visible_limit, last_updated, status, " +
"integrate, top_group, poll_class, push_class, display_class, notify_class, more_messages, server_id, " +
@@ -169,9 +168,6 @@ public class LocalStore {
private final Context context;
private final ContentResolver contentResolver;
- private final MessagePreviewCreator messagePreviewCreator;
- private final MessageFulltextCreator messageFulltextCreator;
- private final AttachmentCounter attachmentCounter;
private final PendingCommandSerializer pendingCommandSerializer;
private final AttachmentInfoExtractor attachmentInfoExtractor;
@@ -186,15 +182,11 @@ public class LocalStore {
/**
* local://localhost/path/to/database/uuid.db
* This constructor is only used by {@link LocalStoreProvider#getInstance(Account)}
- * @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)}
*/
private LocalStore(final Account account, final Context context) throws MessagingException {
this.context = context;
this.contentResolver = context.getContentResolver();
- messagePreviewCreator = MessagePreviewCreator.newInstance();
- messageFulltextCreator = MessageFulltextCreator.newInstance();
- attachmentCounter = AttachmentCounter.newInstance();
pendingCommandSerializer = PendingCommandSerializer.getInstance();
attachmentInfoExtractor = DI.get(AttachmentInfoExtractor.class);
@@ -243,122 +235,6 @@ public class LocalStore {
return outboxStateRepository;
}
- public long getSize() throws MessagingException {
-
- final StorageManager storageManager = StorageManager.getInstance(context);
-
- final File attachmentDirectory = storageManager.getAttachmentDirectory(account.getUuid(),
- database.getStorageProviderId());
-
- return database.execute(false, new DbCallback() {
- @Override
- public Long doDbWork(final SQLiteDatabase db) {
- final File[] files = attachmentDirectory.listFiles();
- long attachmentLength = 0;
- if (files != null) {
- for (File file : files) {
- if (file.exists()) {
- attachmentLength += file.length();
- }
- }
- }
-
- final File dbFile = storageManager.getDatabase(account.getUuid(), database.getStorageProviderId());
- return dbFile.length() + attachmentLength;
- }
- });
- }
-
- public void compact() throws MessagingException {
- if (K9.isDebugLoggingEnabled()) {
- Timber.i("Before compaction size = %d", getSize());
- }
-
- database.execute(false, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
- db.execSQL("VACUUM");
- return null;
- }
- });
-
- if (K9.isDebugLoggingEnabled()) {
- Timber.i("After compaction size = %d", getSize());
- }
- }
-
-
- public void clear() throws MessagingException {
- if (K9.isDebugLoggingEnabled()) {
- Timber.i("Before prune size = %d", getSize());
- }
-
- deleteAllMessageDataFromDisk();
-
- if (K9.isDebugLoggingEnabled()) {
- Timber.i("After prune / before compaction size = %d", getSize());
- Timber.i("Before clear folder count = %d", getFolderCount());
- Timber.i("Before clear message count = %d", getMessageCount());
- Timber.i("After prune / before clear size = %d", getSize());
- }
-
- database.execute(false, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) {
- // We don't care about threads of deleted messages, so delete the whole table.
- db.delete("threads", null, null);
-
- // Don't delete deleted messages. They are essentially placeholders for UIDs of messages that have
- // been deleted locally.
- db.delete("messages", "deleted = 0", null);
-
- // We don't need the search data now either
- db.delete("messages_fulltext", null, null);
-
- return null;
- }
- });
-
- compact();
-
- if (K9.isDebugLoggingEnabled()) {
- Timber.i("After clear message count = %d", getMessageCount());
- Timber.i("After clear size = %d", getSize());
- }
- }
-
- private int getMessageCount() throws MessagingException {
- return database.execute(false, new DbCallback() {
- @Override
- public Integer doDbWork(final SQLiteDatabase db) {
- Cursor cursor = null;
- try {
- cursor = db.rawQuery("SELECT COUNT(*) FROM messages", null);
- cursor.moveToFirst();
- return cursor.getInt(0); // message count
- } finally {
- Utility.closeQuietly(cursor);
- }
- }
- });
- }
-
- private int getFolderCount() throws MessagingException {
- return database.execute(false, new DbCallback() {
- @Override
- public Integer doDbWork(final SQLiteDatabase db) {
- Cursor cursor = null;
- try {
- cursor = db.rawQuery("SELECT COUNT(*) FROM folders", null);
- cursor.moveToFirst();
- return cursor.getInt(0); // folder count
- } finally {
- Utility.closeQuietly(cursor);
- }
- }
- });
- }
-
public LocalFolder getFolder(String serverId) {
return new LocalFolder(this, serverId);
}
@@ -374,79 +250,37 @@ public class LocalStore {
// TODO this takes about 260-300ms, seems slow.
public List getPersonalNamespaces(boolean forceListAll) throws MessagingException {
final List folders = new LinkedList<>();
- try {
- database.execute(false, new DbCallback>() {
- @Override
- public List doDbWork(final SQLiteDatabase db) throws WrappedException {
- Cursor cursor = null;
- try {
- cursor = db.rawQuery("SELECT " + GET_FOLDER_COLS + " FROM folders " +
- "ORDER BY name ASC", null);
- while (cursor.moveToNext()) {
- if (cursor.isNull(FOLDER_ID_INDEX)) {
- continue;
- }
- long folderId = cursor.getLong(FOLDER_ID_INDEX);
- LocalFolder folder = new LocalFolder(LocalStore.this, folderId);
- folder.open(cursor);
-
- folders.add(folder);
+ database.execute(false, new DbCallback>() {
+ @Override
+ public List doDbWork(final SQLiteDatabase db) throws MessagingException {
+ Cursor cursor = null;
+
+ try {
+ cursor = db.rawQuery("SELECT " + GET_FOLDER_COLS + " FROM folders " +
+ "ORDER BY name ASC", null);
+ while (cursor.moveToNext()) {
+ if (cursor.isNull(FOLDER_ID_INDEX)) {
+ continue;
}
- return folders;
- } catch (MessagingException e) {
- throw new WrappedException(e);
- } finally {
- Utility.closeQuietly(cursor);
+ long folderId = cursor.getLong(FOLDER_ID_INDEX);
+ LocalFolder folder = new LocalFolder(LocalStore.this, folderId);
+ folder.open(cursor);
+
+ folders.add(folder);
}
+ return folders;
+ } finally {
+ Utility.closeQuietly(cursor);
}
- });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
- return folders;
- }
-
- public void delete() throws UnavailableStorageException {
- database.delete();
- }
-
- public void recreate() throws UnavailableStorageException {
- database.recreate();
- }
-
- private void deleteAllMessageDataFromDisk() throws MessagingException {
- markAllMessagePartsDataAsMissing();
- deleteAllMessagePartsDataFromDisk();
- }
-
- private void markAllMessagePartsDataAsMissing() throws MessagingException {
- database.execute(false, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
- ContentValues cv = new ContentValues();
- cv.put("data_location", DataLocation.MISSING);
- db.update("message_parts", cv, null, null);
-
- return null;
}
});
- }
- private void deleteAllMessagePartsDataFromDisk() {
- final StorageManager storageManager = StorageManager.getInstance(context);
- File attachmentDirectory = storageManager.getAttachmentDirectory(
- account.getUuid(), database.getStorageProviderId());
- File[] files = attachmentDirectory.listFiles();
- if (files == null) {
- return;
- }
+ return folders;
+ }
- for (File file : files) {
- if (file.exists() && !file.delete()) {
- file.deleteOnExit();
- }
- }
+ public void delete() {
+ database.delete();
}
public void resetVisibleLimits(int visibleLimit) throws MessagingException {
@@ -455,7 +289,7 @@ public class LocalStore {
cv.put("more_messages", MoreMessages.UNKNOWN.getDatabaseName());
database.execute(false, new DbCallback() {
@Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ public Void doDbWork(final SQLiteDatabase db) {
db.update("folders", cv, null, null);
return null;
}
@@ -465,7 +299,7 @@ public class LocalStore {
public List getPendingCommands() throws MessagingException {
return database.execute(false, new DbCallback>() {
@Override
- public List doDbWork(final SQLiteDatabase db) throws WrappedException {
+ public List doDbWork(final SQLiteDatabase db) {
Cursor cursor = null;
try {
cursor = db.query("pending_commands",
@@ -498,7 +332,7 @@ public class LocalStore {
cv.put("data", pendingCommandSerializer.serialize(command));
database.execute(false, new DbCallback() {
@Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ public Void doDbWork(final SQLiteDatabase db) {
db.insert("pending_commands", "command", cv);
return null;
}
@@ -508,7 +342,7 @@ public class LocalStore {
public void removePendingCommand(final PendingCommand command) throws MessagingException {
database.execute(false, new DbCallback() {
@Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ public Void doDbWork(final SQLiteDatabase db) {
db.delete("pending_commands", "id = ?", new String[] { Long.toString(command.databaseId) });
return null;
}
@@ -518,7 +352,7 @@ public class LocalStore {
public void removePendingCommands() throws MessagingException {
database.execute(false, new DbCallback() {
@Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
+ public Void doDbWork(final SQLiteDatabase db) {
db.delete("pending_commands", null, null);
return null;
}
@@ -563,7 +397,7 @@ public class LocalStore {
final List messages = new ArrayList<>();
final int j = database.execute(false, new DbCallback() {
@Override
- public Integer doDbWork(final SQLiteDatabase db) throws WrappedException {
+ public Integer doDbWork(final SQLiteDatabase db) {
Cursor cursor = null;
int i = 0;
try {
@@ -620,7 +454,7 @@ public class LocalStore {
public AttachmentInfo getAttachmentInfo(final String attachmentId) throws MessagingException {
return database.execute(false, new DbCallback() {
@Override
- public AttachmentInfo doDbWork(final SQLiteDatabase db) throws WrappedException {
+ public AttachmentInfo doDbWork(final SQLiteDatabase db) {
Cursor cursor = db.query("message_parts",
new String[] { "display_name", "decoded_body_size", "mime_type" },
"id = ?",
@@ -662,7 +496,7 @@ public class LocalStore {
try {
database.execute(false, new DbCallback() {
@Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException, MessagingException {
+ public Void doDbWork(final SQLiteDatabase db) throws MessagingException {
Cursor cursor = db.query("message_parts",
GET_ATTACHMENT_COLS,
"id = ?", new String[] { partId },
@@ -670,7 +504,7 @@ public class LocalStore {
try {
writeCursorPartsToOutputStream(db, cursor, outputStream);
} catch (IOException e) {
- throw new WrappedException(e);
+ throw new MessagingException(e);
} finally {
Utility.closeQuietly(cursor);
}
@@ -680,8 +514,6 @@ public class LocalStore {
});
} catch (MessagingException e) {
throw new IOException("Got a MessagingException while writing attachment data!", e);
- } catch (WrappedException e) {
- throw (IOException) e.getCause();
}
}
@@ -859,36 +691,6 @@ public class LocalStore {
return new File(attachmentDirectory, attachmentId);
}
- public String getFolderServerId(long folderId) throws MessagingException {
- return database.execute(false, db -> {
- try (Cursor cursor = db.query("folders", new String[] { "server_id" },
- "id = ?", new String[] { Long.toString(folderId) },
- null, null, null)
- ) {
- if (cursor.moveToFirst() && !cursor.isNull(0)) {
- return cursor.getString(0);
- } else {
- throw new MessagingException("Folder not found by database ID: " + folderId, true);
- }
- }
- });
- }
-
- public long getFolderId(String folderServerId) throws MessagingException {
- return database.execute(false, db -> {
- try (Cursor cursor = db.query("folders", new String[] { "id" },
- "server_id = ?", new String[] { folderServerId },
- null, null, null)
- ) {
- if (cursor.moveToFirst()) {
- return cursor.getLong(0);
- } else {
- throw new MessagingException("Folder not found by server ID: " + folderServerId);
- }
- }
- });
- }
-
public static class AttachmentInfo {
public String name;
public long size;
@@ -936,18 +738,6 @@ public class LocalStore {
return database;
}
- MessagePreviewCreator getMessagePreviewCreator() {
- return messagePreviewCreator;
- }
-
- public MessageFulltextCreator getMessageFulltextCreator() {
- return messageFulltextCreator;
- }
-
- AttachmentCounter getAttachmentCounter() {
- return attachmentCounter;
- }
-
AttachmentInfoExtractor getAttachmentInfoExtractor() {
return attachmentInfoExtractor;
}
@@ -996,24 +786,18 @@ public class LocalStore {
selection.append(")");
- try {
- database.execute(true, new DbCallback() {
- @Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
- UnavailableStorageException {
-
- selectionCallback.doDbWork(db, selection.toString(),
- selectionArgs.toArray(new String[selectionArgs.size()]));
+ database.execute(true, new DbCallback() {
+ @Override
+ public Void doDbWork(final SQLiteDatabase db) {
- return null;
- }
- });
+ selectionCallback.doDbWork(db, selection.toString(),
+ selectionArgs.toArray(new String[selectionArgs.size()]));
- selectionCallback.postDbWork();
+ return null;
+ }
+ });
- } catch (WrappedException e) {
- throw(MessagingException) e.getCause();
- }
+ selectionCallback.postDbWork();
selectionArgs.clear();
start += count;
@@ -1050,8 +834,7 @@ public class LocalStore {
* @param selectionArgs
* The current subset of the argument list.
*/
- void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
- throws UnavailableStorageException;
+ void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs);
/**
* This will be executed after each invocation of
@@ -1095,8 +878,7 @@ public class LocalStore {
}
@Override
- public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
- throws UnavailableStorageException {
+ public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) {
db.update("messages", cv, "empty = 0 AND id" + selectionSet,
selectionArgs);
@@ -1142,8 +924,7 @@ public class LocalStore {
}
@Override
- public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
- throws UnavailableStorageException {
+ public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) {
db.execSQL("UPDATE messages SET " + flagColumn + " = " + ((newState) ? "1" : "0") +
" WHERE id IN (" +
@@ -1193,8 +974,7 @@ public class LocalStore {
}
@Override
- public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
- throws UnavailableStorageException {
+ public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) {
if (threadedList) {
String sql = "SELECT m.uid, m.folder_id " +
@@ -1260,7 +1040,7 @@ public class LocalStore {
return database.execute(false, new DbCallback() {
@Override
- public Integer doDbWork(SQLiteDatabase db) throws WrappedException, MessagingException {
+ public Integer doDbWork(SQLiteDatabase db) {
Cursor cursor = db.rawQuery(sqlQuery, selectionArgs);
try {
if (cursor.moveToFirst()) {
@@ -1275,6 +1055,69 @@ public class LocalStore {
});
}
+ private int getStarredMessageCount(LocalSearch search) throws MessagingException {
+ StringBuilder whereBuilder = new StringBuilder();
+ List queryArgs = new ArrayList<>();
+ SqlQueryBuilder.buildWhereClause(account, search.getConditions(), whereBuilder, queryArgs);
+
+ String where = whereBuilder.toString();
+ final String[] selectionArgs = queryArgs.toArray(new String[queryArgs.size()]);
+
+ final String sqlQuery = "SELECT SUM(flagged=1) " +
+ "FROM messages " +
+ "JOIN folders ON (folders.id = messages.folder_id) " +
+ "WHERE (messages.empty = 0 AND messages.deleted = 0)" +
+ (!TextUtils.isEmpty(where) ? " AND (" + where + ")" : "");
+
+ return database.execute(false, new DbCallback() {
+ @Override
+ public Integer doDbWork(SQLiteDatabase db) {
+ Cursor cursor = db.rawQuery(sqlQuery, selectionArgs);
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getInt(0);
+ } else {
+ return 0;
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ });
+ }
+
+ public MessageCounts getMessageCounts(LocalSearch search) throws MessagingException {
+ return new MessageCounts(getUnreadMessageCount(search), getStarredMessageCount(search));
+ }
+
+ public List getNotificationMessages() throws MessagingException {
+ return database.execute(false, db -> {
+ try (Cursor cursor = db.rawQuery(
+ "SELECT " + GET_MESSAGES_COLS + ", notifications.notification_id, notifications.timestamp " +
+ "FROM notifications " +
+ "JOIN messages ON (messages.id = notifications.message_id) " +
+ "LEFT JOIN threads ON (threads.message_id = messages.id) " +
+ "LEFT JOIN message_parts ON (message_parts.id = messages.message_part_id) " +
+ "LEFT JOIN folders ON (folders.id = messages.folder_id) " +
+ "ORDER BY notifications.timestamp DESC", null)
+ ) {
+ List messages = new ArrayList<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ long folderId = cursor.getLong(MSG_INDEX_FOLDER_ID);
+ LocalFolder folder = getFolder(folderId);
+ LocalMessage message = new LocalMessage(LocalStore.this, null, folder);
+ message.populateFromGetMessageCursor(cursor);
+
+ Integer notificationId = CursorKt.getIntOrNull(cursor, MSG_INDEX_NOTIFICATION_ID);
+ long notificationTimeStamp = cursor.getLong(MSG_INDEX_NOTIFICATION_TIMESTAMP);
+
+ messages.add(new NotificationMessage(message, notificationId, notificationTimeStamp));
+ }
+
+ return messages;
+ }
+ });
+ }
public static String getColumnNameForFlag(Flag flag) {
switch (flag) {
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/LockableDatabase.java b/app/core/src/main/java/com/fsck/k9/mailstore/LockableDatabase.java
index d846f01dab84cce32d07d950f09c7ecbe2aac4cb..72efef185de4f74fbab6a30074bb16a4e97c6e53 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/LockableDatabase.java
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/LockableDatabase.java
@@ -33,11 +33,8 @@ public class LockableDatabase {
* The locked database on which the work should occur. Never
* null
.
* @return Any relevant data. Can be null
.
- * @throws WrappedException
- * @throws com.fsck.k9.mail.MessagingException
- * @throws com.fsck.k9.mailstore.UnavailableStorageException
*/
- T doDbWork(SQLiteDatabase db) throws WrappedException, MessagingException;
+ T doDbWork(SQLiteDatabase db) throws MessagingException;
}
public interface SchemaDefinition {
@@ -49,61 +46,6 @@ public class LockableDatabase {
void doDbUpgrade(SQLiteDatabase db);
}
- /**
- * Workaround exception wrapper used to keep the inner exception generated
- * in a {@link DbCallback}.
- */
- public static class WrappedException extends RuntimeException {
- /**
- *
- */
- private static final long serialVersionUID = 8184421232587399369L;
-
- public WrappedException(final Exception cause) {
- super(cause);
- }
- }
-
- /**
- * Open the DB on mount and close the DB on unmount
- */
- private class StorageListener implements StorageManager.StorageListener {
- @Override
- public void onUnmount(final String providerId) {
- if (!providerId.equals(mStorageProviderId)) {
- return;
- }
-
- Timber.d("LockableDatabase: Closing DB %s due to unmount event on StorageProvider: %s", uUid, providerId);
-
- try {
- lockWrite();
- try {
- mDb.close();
- } finally {
- unlockWrite();
- }
- } catch (UnavailableStorageException e) {
- Timber.w(e, "Unable to writelock on unmount");
- }
- }
-
- @Override
- public void onMount(final String providerId) {
- if (!providerId.equals(mStorageProviderId)) {
- return;
- }
-
- Timber.d("LockableDatabase: Opening DB %s due to mount event on StorageProvider: %s", uUid, providerId);
-
- try {
- openOrCreateDataspace();
- } catch (UnavailableStorageException e) {
- Timber.e(e, "Unable to open DB on mount");
- }
- }
- }
-
private String mStorageProviderId;
private SQLiteDatabase mDb;
@@ -123,8 +65,6 @@ public class LockableDatabase {
mWriteLock = lock.writeLock();
}
- private final StorageListener mStorageListener = new StorageListener();
-
private Context context;
/**
@@ -173,22 +113,12 @@ public class LockableDatabase {
* You have to invoke {@link #unlockRead()} when you're
* done with the storage.
*
- *
- * @throws UnavailableStorageException
- * If storage can't be locked because it is not available
*/
- protected void lockRead() throws UnavailableStorageException {
+ protected void lockRead() {
mReadLock.lock();
- try {
- getStorageManager().lockProvider(mStorageProviderId);
- } catch (UnavailableStorageException | RuntimeException e) {
- mReadLock.unlock();
- throw e;
- }
}
protected void unlockRead() {
- getStorageManager().unlockProvider(mStorageProviderId);
mReadLock.unlock();
}
@@ -200,45 +130,12 @@ public class LockableDatabase {
* You have to invoke {@link #unlockWrite()} when you're
* done with the storage.
*
- *
- * @throws UnavailableStorageException
- * If storage can't be locked because it is not available.
*/
- protected void lockWrite() throws UnavailableStorageException {
- lockWrite(mStorageProviderId);
- }
-
- /**
- * Lock the storage for exclusive access (other threads aren't allowed to
- * run simultaneously)
- *
- *
- * You have to invoke {@link #unlockWrite()} when you're
- * done with the storage.
- *
- *
- * @param providerId
- * Never null
.
- *
- * @throws UnavailableStorageException
- * If storage can't be locked because it is not available.
- */
- protected void lockWrite(final String providerId) throws UnavailableStorageException {
+ private void lockWrite() {
mWriteLock.lock();
- try {
- getStorageManager().lockProvider(providerId);
- } catch (UnavailableStorageException | RuntimeException e) {
- mWriteLock.unlock();
- throw e;
- }
}
- protected void unlockWrite() {
- unlockWrite(mStorageProviderId);
- }
-
- protected void unlockWrite(final String providerId) {
- getStorageManager().unlockProvider(providerId);
+ private void unlockWrite() {
mWriteLock.unlock();
}
@@ -258,7 +155,6 @@ public class LockableDatabase {
* @param callback
* Never null
.
* @return Whatever {@link DbCallback#doDbWork(SQLiteDatabase)} returns.
- * @throws UnavailableStorageException
*/
public T execute(final boolean transactional, final DbCallback callback) throws MessagingException {
lockRead();
@@ -314,58 +210,47 @@ public class LockableDatabase {
Timber.v("LockableDatabase: Switching provider from %s to %s", mStorageProviderId, newProviderId);
final String oldProviderId = mStorageProviderId;
- lockWrite(oldProviderId);
+ lockWrite();
try {
- lockWrite(newProviderId);
try {
- try {
- mDb.close();
- } catch (Exception e) {
- Timber.i(e, "Unable to close DB on local store migration");
- }
+ mDb.close();
+ } catch (Exception e) {
+ Timber.i(e, "Unable to close DB on local store migration");
+ }
- final StorageManager storageManager = getStorageManager();
- File oldDatabase = storageManager.getDatabase(uUid, oldProviderId);
+ final StorageManager storageManager = getStorageManager();
+ File oldDatabase = storageManager.getDatabase(uUid, oldProviderId);
- // create new path
- prepareStorage(newProviderId);
+ // create new path
+ prepareStorage(newProviderId);
- // move all database files
- FileHelper.moveRecursive(oldDatabase, storageManager.getDatabase(uUid, newProviderId));
- // move all attachment files
- FileHelper.moveRecursive(storageManager.getAttachmentDirectory(uUid, oldProviderId),
- storageManager.getAttachmentDirectory(uUid, newProviderId));
- // remove any remaining old journal files
- deleteDatabase(oldDatabase);
+ // move all database files
+ FileHelper.moveRecursive(oldDatabase, storageManager.getDatabase(uUid, newProviderId));
+ // move all attachment files
+ FileHelper.moveRecursive(storageManager.getAttachmentDirectory(uUid, oldProviderId),
+ storageManager.getAttachmentDirectory(uUid, newProviderId));
+ // remove any remaining old journal files
+ deleteDatabase(oldDatabase);
- mStorageProviderId = newProviderId;
+ mStorageProviderId = newProviderId;
- // re-initialize this class with the new Uri
- openOrCreateDataspace();
- } finally {
- unlockWrite(newProviderId);
- }
+ // re-initialize this class with the new Uri
+ openOrCreateDataspace();
} finally {
- unlockWrite(oldProviderId);
+ unlockWrite();
}
}
- public void open() throws UnavailableStorageException {
+ public void open() {
lockWrite();
try {
openOrCreateDataspace();
} finally {
unlockWrite();
}
- StorageManager.getInstance(context).addListener(mStorageListener);
}
- /**
- *
- * @throws UnavailableStorageException
- */
- private void openOrCreateDataspace() throws UnavailableStorageException {
-
+ private void openOrCreateDataspace() {
lockWrite();
try {
final File databaseFile = prepareStorage(mStorageProviderId);
@@ -401,13 +286,7 @@ public class LockableDatabase {
}
}
- /**
- * @param providerId
- * Never null
.
- * @return DB file.
- * @throws UnavailableStorageException
- */
- protected File prepareStorage(final String providerId) throws UnavailableStorageException {
+ protected File prepareStorage(final String providerId) {
final StorageManager storageManager = getStorageManager();
final File databaseFile = storageManager.getDatabase(uUid, providerId);
@@ -419,8 +298,7 @@ public class LockableDatabase {
}
if (!databaseParentDir.exists()) {
if (!databaseParentDir.mkdirs()) {
- // Android seems to be unmounting the storage...
- throw new UnavailableStorageException("Unable to access: " + databaseParentDir);
+ throw new RuntimeException("Unable to access: " + databaseParentDir);
}
FileHelper.touchFile(databaseParentDir, ".nomedia");
}
@@ -441,23 +319,20 @@ public class LockableDatabase {
/**
* Delete the backing database.
- *
- * @throws UnavailableStorageException
*/
- public void delete() throws UnavailableStorageException {
+ public void delete() {
delete(false);
}
- public void recreate() throws UnavailableStorageException {
+ public void recreate() {
delete(true);
}
/**
* @param recreate
* true
if the DB should be recreated after delete
- * @throws UnavailableStorageException
*/
- private void delete(final boolean recreate) throws UnavailableStorageException {
+ private void delete(final boolean recreate) {
lockWrite();
try {
try {
@@ -494,9 +369,6 @@ public class LockableDatabase {
if (recreate) {
openOrCreateDataspace();
- } else {
- // stop waiting for mount/unmount events
- getStorageManager().removeListener(mStorageListener);
}
} finally {
unlockWrite();
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt
index e59aeaebb0ae0a1d6dde87de39315ba288dff086..9294ac736f5ec052342cde7cf7d8c772f3f87deb 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt
@@ -78,6 +78,16 @@ interface MessageStore {
*/
fun setMessageFlag(folderId: Long, messageServerId: String, flag: Flag, set: Boolean)
+ /**
+ * Set whether a message should be considered as new.
+ */
+ fun setNewMessageState(folderId: Long, messageServerId: String, newMessage: Boolean)
+
+ /**
+ * Clear the new message state for all messages.
+ */
+ fun clearNewMessageState()
+
/**
* Retrieve the server ID for a given message.
*/
@@ -121,9 +131,9 @@ interface MessageStore {
fun getHeaders(folderId: Long, messageServerId: String): List
/**
- * Get highest UID (message server ID)
+ * Return the size of this message store in bytes.
*/
- fun getLastUid(folderId: Long): Long?
+ fun getSize(): Long
/**
* Remove messages from the store.
@@ -166,11 +176,26 @@ interface MessageStore {
*/
fun getDisplayFolders(displayMode: FolderMode, outboxFolderId: Long?, mapper: FolderMapper): List
+ /**
+ * Check if all given folders are included in the Unified Inbox.
+ */
+ fun areAllIncludedInUnifiedInbox(folderIds: Collection): Boolean
+
/**
* Find a folder with the given server ID and return its store ID.
*/
fun getFolderId(folderServerId: String): Long?
+ /**
+ * Find a folder with the given store ID and return its server ID.
+ */
+ fun getFolderServerId(folderId: Long): String?
+
+ /**
+ * Retrieve the number of messages in a folder.
+ */
+ fun getMessageCount(folderId: Long): Int
+
/**
* Update a folder's name and type.
*/
@@ -212,9 +237,9 @@ interface MessageStore {
fun setMoreMessages(folderId: Long, moreMessages: MoreMessages)
/**
- * Update the 'last updated' state of a folder.
+ * Update the time when the folder was last checked for new messages.
*/
- fun setLastUpdated(folderId: Long, timestamp: Long)
+ fun setLastChecked(folderId: Long, timestamp: Long)
/**
* Update folder status message.
@@ -267,4 +292,9 @@ interface MessageStore {
* Create or update a number property associated with the given folder.
*/
fun setFolderExtraNumber(folderId: Long, name: String, value: Long)
+
+ /**
+ * Optimize the message store with the goal of using the minimal amount of disk space.
+ */
+ fun compact()
}
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java b/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java
index 35795fff237fdf17b5b5073c14d10b68a4dce8af..74e66529b86ee4407b6468dad3e222bcc0b18b6d 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java
@@ -66,7 +66,8 @@ public class MessageViewInfo {
}
public static MessageViewInfo createForMetadataOnly(Message message, boolean isMessageIncomplete) {
- return new MessageViewInfo(message, isMessageIncomplete, null, null, false, null, null, null, null, null, null);
+ String subject = message.getSubject();
+ return new MessageViewInfo(message, isMessageIncomplete, null, subject, false, null, null, null, null, null, null);
}
MessageViewInfo withCryptoData(CryptoResultAnnotation rootPartAnnotation, String extraViewableText,
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java b/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java
index 4499e955b90033b7996650b3b00e7fdb3fc9b3ec..a3a5cead774b2fca8bfaa04f34ec7c11433b35e9 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java
@@ -20,9 +20,7 @@ import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.Viewable;
-import com.fsck.k9.mail.internet.Viewable.Flowed;
import com.fsck.k9.mailstore.CryptoResultAnnotation.CryptoError;
-import com.fsck.k9.mailstore.util.FlowedMessageUtils;
import com.fsck.k9.message.extractors.AttachmentInfoExtractor;
import com.fsck.k9.message.html.HtmlConverter;
import com.fsck.k9.message.html.HtmlProcessor;
@@ -271,10 +269,6 @@ public class MessageViewInfoExtractor {
String t = getTextFromPart(part);
if (t == null) {
t = "";
- } else if (viewable instanceof Flowed) {
- boolean delSp = ((Flowed) viewable).isDelSp();
- t = FlowedMessageUtils.deflow(t, delSp);
- t = HtmlConverter.textToHtml(t);
} else if (viewable instanceof Text) {
t = HtmlConverter.textToHtml(t);
} else if (!(viewable instanceof Html)) {
@@ -310,9 +304,6 @@ public class MessageViewInfoExtractor {
t = "";
} else if (viewable instanceof Html) {
t = HtmlConverter.htmlToText(t);
- } else if (viewable instanceof Flowed) {
- boolean delSp = ((Flowed) viewable).isDelSp();
- t = FlowedMessageUtils.deflow(t, delSp);
} else if (!(viewable instanceof Text)) {
throw new IllegalStateException("unhandled case!");
}
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/NotificationMessage.kt b/app/core/src/main/java/com/fsck/k9/mailstore/NotificationMessage.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dddb1073c8e61da65a54f5fb31d06689498e37c1
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/NotificationMessage.kt
@@ -0,0 +1,7 @@
+package com.fsck.k9.mailstore
+
+data class NotificationMessage(
+ val message: LocalMessage,
+ val notificationId: Int?,
+ val timestamp: Long
+)
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/NotifierMessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/NotifierMessageStore.kt
index f3bd1276ab9c8096cc208d329e04b05ce3b5ec37..488dbbffd3662c65d5756832a5a64f9fd160cc09 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/NotifierMessageStore.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/NotifierMessageStore.kt
@@ -43,6 +43,16 @@ class NotifierMessageStore(
notifyChange()
}
+ override fun setNewMessageState(folderId: Long, messageServerId: String, newMessage: Boolean) {
+ messageStore.setNewMessageState(folderId, messageServerId, newMessage)
+ notifyChange()
+ }
+
+ override fun clearNewMessageState() {
+ messageStore.clearNewMessageState()
+ notifyChange()
+ }
+
override fun destroyMessages(folderId: Long, messageServerIds: Collection) {
messageStore.destroyMessages(folderId, messageServerIds)
notifyChange()
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/OutboxStateRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/OutboxStateRepository.kt
index 794e93af637bb141cfedd50a5c34cf1648f0829f..c2c11aa932f6977b9b137d310384c7c2cbc6609a 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/OutboxStateRepository.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/OutboxStateRepository.kt
@@ -2,6 +2,10 @@ package com.fsck.k9.mailstore
import android.content.ContentValues
import com.fsck.k9.Clock
+import com.fsck.k9.helper.getIntOrThrow
+import com.fsck.k9.helper.getLongOrThrow
+import com.fsck.k9.helper.getStringOrNull
+import com.fsck.k9.helper.getStringOrThrow
class OutboxStateRepository(private val database: LockableDatabase, private val clock: Clock) {
@@ -17,11 +21,10 @@ class OutboxStateRepository(private val database: LockableDatabase, private val
throw IllegalStateException("No outbox_state entry for message with id $messageId")
}
- val sendStateString = cursor.getString(cursor.getColumnIndex(COLUMN_SEND_STATE))
- val numberOfSendAttempts = cursor.getInt(cursor.getColumnIndex(COLUMN_NUMBER_OF_SEND_ATTEMPTS))
- val sendErrorTimestamp = cursor.getLong(cursor.getColumnIndex(COLUMN_ERROR_TIMESTAMP))
- val sendErrorColumnIndex = cursor.getColumnIndex(COLUMN_ERROR)
- val sendError = if (cursor.isNull(sendErrorColumnIndex)) null else cursor.getString(sendErrorColumnIndex)
+ val sendStateString = cursor.getStringOrThrow(COLUMN_SEND_STATE)
+ val numberOfSendAttempts = cursor.getIntOrThrow(COLUMN_NUMBER_OF_SEND_ATTEMPTS)
+ val sendErrorTimestamp = cursor.getLongOrThrow(COLUMN_ERROR_TIMESTAMP)
+ val sendError = cursor.getStringOrNull(COLUMN_ERROR)
val sendState = SendState.fromDatabaseName(sendStateString)
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/SpecialFolderUpdater.kt b/app/core/src/main/java/com/fsck/k9/mailstore/SpecialFolderUpdater.kt
index 908699a8bbb67213e27bb538914719974c1a926e..e25f08c7767681e830fe2b62a4055102a78bb444 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/SpecialFolderUpdater.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/SpecialFolderUpdater.kt
@@ -18,7 +18,7 @@ class SpecialFolderUpdater(
private val account: Account
) {
fun updateSpecialFolders() {
- val folders = folderRepository.getRemoteFolders()
+ val folders = folderRepository.getRemoteFolders(account)
updateInbox(folders)
@@ -42,15 +42,15 @@ class SpecialFolderUpdater(
account.inboxFolderId = newInboxId
if (oldInboxId != null && folders.any { it.id == oldInboxId }) {
- folderRepository.setIncludeInUnifiedInbox(oldInboxId, false)
+ folderRepository.setIncludeInUnifiedInbox(account, oldInboxId, false)
}
if (newInboxId != null) {
- folderRepository.setIncludeInUnifiedInbox(newInboxId, true)
- folderRepository.setDisplayClass(newInboxId, FolderClass.FIRST_CLASS)
- folderRepository.setSyncClass(newInboxId, FolderClass.FIRST_CLASS)
- folderRepository.setPushClass(newInboxId, FolderClass.FIRST_CLASS)
- folderRepository.setNotificationClass(newInboxId, FolderClass.FIRST_CLASS)
+ folderRepository.setIncludeInUnifiedInbox(account, newInboxId, true)
+ folderRepository.setDisplayClass(account, newInboxId, FolderClass.FIRST_CLASS)
+ folderRepository.setSyncClass(account, newInboxId, FolderClass.FIRST_CLASS)
+ folderRepository.setPushClass(account, newInboxId, FolderClass.FIRST_CLASS)
+ folderRepository.setNotificationClass(account, newInboxId, FolderClass.FIRST_CLASS)
}
}
@@ -117,8 +117,8 @@ class SpecialFolderUpdater(
}
if (folderId != null) {
- folderRepository.setDisplayClass(folderId, FolderClass.FIRST_CLASS)
- folderRepository.setSyncClass(folderId, FolderClass.NO_CLASS)
+ folderRepository.setDisplayClass(account, folderId, FolderClass.FIRST_CLASS)
+ folderRepository.setSyncClass(account, folderId, FolderClass.NO_CLASS)
}
}
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/StorageManager.java b/app/core/src/main/java/com/fsck/k9/mailstore/StorageManager.java
index 5e1ca29a19337ef6b4dc4eb7f9bc37455efec4bd..6aece52ec306c41a7142228f1f6e6ef5f45899ce 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/StorageManager.java
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/StorageManager.java
@@ -2,24 +2,15 @@ package com.fsck.k9.mailstore;
import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
import java.util.Arrays;
-import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.Set;
import android.content.Context;
import android.os.Environment;
-import com.fsck.k9.Core;
-import com.fsck.k9.CoreResourceProvider;
-import com.fsck.k9.DI;
-import timber.log.Timber;
/**
* Manager for different {@link StorageProvider} -classes that abstract access
@@ -64,14 +55,6 @@ public class StorageManager {
*/
void init(Context context);
- /**
- * @param context
- * Never null
.
- * @return A user displayable, localized name for this provider. Never
- * null
.
- */
- String getName(Context context);
-
/**
* Some implementations may not be able to return valid File handles
* because the device doesn't provide the denoted storage. You can check
@@ -110,135 +93,6 @@ public class StorageManager {
* @return Never null
.
*/
File getAttachmentDirectory(Context context, String id);
-
- /**
- * Check for the underlying storage availability.
- *
- * @param context
- * Never null
.
- * @return Whether the underlying storage returned by this provider is
- * ready for read/write operations at the time of invocation.
- */
- boolean isReady(Context context);
-
- /**
- * Retrieve the root of the underlying storage.
- *
- * @param context
- * Never null
.
- * @return The root directory of the denoted storage. Never
- * null
.
- */
- File getRoot(Context context);
- }
-
- /**
- * Interface for components wanting to be notified of storage availability
- * events.
- */
- public interface StorageListener {
- /**
- * Invoked on storage mount (with read/write access).
- *
- * @param providerId
- * Identifier (as returned by {@link StorageProvider#getId()}
- * of the newly mounted storage. Never null
.
- */
- void onMount(String providerId);
-
- /**
- * Invoked when a storage is about to be unmounted.
- *
- * @param providerId
- * Identifier (as returned by {@link StorageProvider#getId()}
- * of the to-be-unmounted storage. Never null
.
- */
- void onUnmount(String providerId);
- }
-
- /**
- * Base provider class for providers that rely on well-known path to check
- * for storage availability.
- *
- *
- * Since solely checking for paths can be unsafe, this class allows to check
- * for device compatibility using {@link #supportsVendor()}. If the vendor
- * specific check fails, the provider won't be able to provide any valid
- * File handle, regardless of the path existence.
- *
- *
- *
- * Moreover, this class validates the denoted storage path against mount
- * points using {@link StorageManager#isMountPoint(File)}.
- *
- */
- public abstract static class FixedStorageProviderBase implements StorageProvider {
- /**
- * The root of the denoted storage. Used for mount points checking.
- */
- private File mRoot;
-
- /**
- * Chosen base directory
- */
- private File mApplicationDir;
-
- @Override
- public void init(final Context context) {
- mRoot = computeRoot(context);
- // use /k9
- mApplicationDir = new File(mRoot, "k9");
- }
-
- /**
- * Vendor specific checks
- *
- * @return Whether this provider supports the underlying vendor specific
- * storage
- */
- protected abstract boolean supportsVendor();
-
- @Override
- public boolean isReady(Context context) {
- try {
- final File root = mRoot.getCanonicalFile();
- return isMountPoint(root)
- && Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
- } catch (IOException e) {
- Timber.w(e, "Specified root isn't ready: %s", mRoot);
- return false;
- }
- }
-
- @Override
- public final boolean isSupported(Context context) {
- return mRoot.isDirectory() && supportsVendor();
- }
-
- @Override
- public File getDatabase(Context context, String id) {
- return new File(mApplicationDir, id + ".db");
- }
-
- @Override
- public File getAttachmentDirectory(Context context, String id) {
- return new File(mApplicationDir, id + ".db_att");
- }
-
- @Override
- public final File getRoot(Context context) {
- return mRoot;
- }
-
- /**
- * Retrieve the well-known storage root directory from the actual
- * implementation.
- *
- * @param context
- * Never null
.
- * @return Never null
.
- */
- protected abstract File computeRoot(Context context);
}
/**
@@ -257,13 +111,6 @@ public class StorageManager {
public static class InternalStorageProvider implements StorageProvider {
public static final String ID = "InternalStorage";
- private final CoreResourceProvider resourceProvider;
- private File mRoot;
-
- public InternalStorageProvider(CoreResourceProvider resourceProvider) {
- this.resourceProvider = resourceProvider;
- }
-
@Override
public String getId() {
return ID;
@@ -271,13 +118,6 @@ public class StorageManager {
@Override
public void init(Context context) {
- // XXX
- mRoot = new File("/");
- }
-
- @Override
- public String getName(Context context) {
- return resourceProvider.internalStorageProviderName();
}
@Override
@@ -295,16 +135,6 @@ public class StorageManager {
// we store attachments in the database directory
return context.getDatabasePath(id + ".db_att");
}
-
- @Override
- public boolean isReady(Context context) {
- return true;
- }
-
- @Override
- public File getRoot(Context context) {
- return mRoot;
- }
}
/**
@@ -328,18 +158,12 @@ public class StorageManager {
public static class ExternalStorageProvider implements StorageProvider {
public static final String ID = "ExternalStorage";
- private final CoreResourceProvider resourceProvider;
-
/**
* Chosen base directory.
*/
private File mApplicationDirectory;
- public ExternalStorageProvider(CoreResourceProvider resourceProvider) {
- this.resourceProvider = resourceProvider;
- }
-
@Override
public String getId() {
return ID;
@@ -350,11 +174,6 @@ public class StorageManager {
mApplicationDirectory = context.getExternalFilesDir(null);
}
- @Override
- public String getName(Context context) {
- return resourceProvider.externalStorageProviderName();
- }
-
@Override
public boolean isSupported(Context context) {
return true;
@@ -369,37 +188,6 @@ public class StorageManager {
public File getAttachmentDirectory(Context context, String id) {
return new File(mApplicationDirectory, id + ".db_att");
}
-
- @Override
- public boolean isReady(Context context) {
- return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
- }
-
- @Override
- public File getRoot(Context context) {
- return Environment.getExternalStorageDirectory();
- }
- }
-
- /**
- * Stores storage provider locking information
- */
- public static class SynchronizationAid {
- /**
- * {@link Lock} has a thread semantic so it can't be released from
- * another thread - this flags act as a holder for the unmount state
- */
- public boolean unmounting = false;
-
- public final Lock readLock;
-
- public final Lock writeLock;
-
- {
- final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
- readLock = readWriteLock.readLock();
- writeLock = readWriteLock.writeLock();
- }
}
/**
@@ -407,51 +195,25 @@ public class StorageManager {
*/
private final Map mProviders = new LinkedHashMap<>();
- /**
- * Locking data for the active storage providers.
- */
- private final Map mProviderLocks = new IdentityHashMap<>();
-
protected final Context context;
- /**
- * Listener to be notified for storage related events.
- */
- private List mListeners = new ArrayList<>();
-
private static transient StorageManager instance;
public static synchronized StorageManager getInstance(final Context context) {
if (instance == null) {
Context applicationContext = context.getApplicationContext();
- CoreResourceProvider resourceProvider = DI.get(CoreResourceProvider.class);
- instance = new StorageManager(applicationContext, resourceProvider);
+ instance = new StorageManager(applicationContext);
}
return instance;
}
- /**
- * @param file
- * Canonical file to match. Never null
.
- * @return Whether the specified file matches a filesystem root.
- * @throws IOException
- */
- public static boolean isMountPoint(final File file) {
- for (final File root : File.listRoots()) {
- if (root.equals(file)) {
- return true;
- }
- }
- return false;
- }
-
/**
* @param context
* Never null
.
* @throws NullPointerException
* If context is null
.
*/
- protected StorageManager(final Context context, CoreResourceProvider resourceProvider) throws NullPointerException {
+ protected StorageManager(final Context context) throws NullPointerException {
if (context == null) {
throw new NullPointerException("No Context given");
}
@@ -468,8 +230,9 @@ public class StorageManager {
* be considered as the default provider !!!
*/
final List allProviders = Arrays.asList(
- new InternalStorageProvider(resourceProvider),
- new ExternalStorageProvider(resourceProvider));
+ new InternalStorageProvider(),
+ new ExternalStorageProvider()
+ );
for (final StorageProvider provider : allProviders) {
// check for provider compatibility
if (provider.isSupported(context)) {
@@ -477,7 +240,6 @@ public class StorageManager {
provider.init(context);
mProviders.put(provider.getId(), provider);
- mProviderLocks.put(provider, new SynchronizationAid());
}
}
@@ -526,152 +288,7 @@ public class StorageManager {
return provider.getAttachmentDirectory(context, dbName);
}
- /**
- * @param providerId
- * Never null
.
- * @return Whether the specified provider is ready for read/write operations
- */
- public boolean isReady(final String providerId) {
- StorageProvider provider = getProvider(providerId);
- if (provider == null) {
- Timber.w("Storage-Provider \"%s\" does not exist", providerId);
- return false;
- }
- return provider.isReady(context);
- }
-
- /**
- * @return A map of available providers names, indexed by their ID. Never
- * null
.
- * @see StorageManager
- * @see StorageProvider#isSupported(Context)
- */
- public Map getAvailableProviders() {
- final Map result = new LinkedHashMap<>();
- for (final Map.Entry entry : mProviders.entrySet()) {
- result.put(entry.getKey(), entry.getValue().getName(context));
- }
- return result;
- }
-
- /**
- * @param path
- */
- public void onBeforeUnmount(final String path) {
- Timber.i("storage path \"%s\" unmounting", path);
- final StorageProvider provider = resolveProvider(path);
- if (provider == null) {
- return;
- }
- for (final StorageListener listener : mListeners) {
- try {
- listener.onUnmount(provider.getId());
- } catch (Exception e) {
- Timber.w(e, "Error while notifying StorageListener");
- }
- }
- final SynchronizationAid sync = mProviderLocks.get(resolveProvider(path));
- sync.writeLock.lock();
- sync.unmounting = true;
- sync.writeLock.unlock();
- }
-
- public void onAfterUnmount(final String path) {
- Timber.i("storage path \"%s\" unmounted", path);
- final StorageProvider provider = resolveProvider(path);
- if (provider == null) {
- return;
- }
- final SynchronizationAid sync = mProviderLocks.get(resolveProvider(path));
- sync.writeLock.lock();
- sync.unmounting = false;
- sync.writeLock.unlock();
-
- Core.setServicesEnabled(context);
- }
-
- /**
- * @param path
- * @param readOnly
- */
- public void onMount(final String path, final boolean readOnly) {
- Timber.i("storage path \"%s\" mounted readOnly=%s", path, readOnly);
- if (readOnly) {
- return;
- }
-
- final StorageProvider provider = resolveProvider(path);
- if (provider == null) {
- return;
- }
- for (final StorageListener listener : mListeners) {
- try {
- listener.onMount(provider.getId());
- } catch (Exception e) {
- Timber.w(e, "Error while notifying StorageListener");
- }
- }
-
- // XXX we should reset mail service ONLY if there are accounts using the storage (this is not done in a regular listener because it has to be invoked afterward)
- Core.setServicesEnabled(context);
- }
-
- /**
- * @param path
- * Never null
.
- * @return The corresponding provider. null
if no match.
- */
- protected StorageProvider resolveProvider(final String path) {
- for (final StorageProvider provider : mProviders.values()) {
- if (path.equals(provider.getRoot(context).getAbsolutePath())) {
- return provider;
- }
- }
- return null;
- }
-
- public void addListener(final StorageListener listener) {
- mListeners.add(listener);
- }
-
- public void removeListener(final StorageListener listener) {
- mListeners.remove(listener);
- }
-
- /**
- * Try to lock the underlying storage to prevent concurrent unmount.
- *
- *
- * You must invoke {@link #unlockProvider(String)} when you're done with the
- * storage.
- *
- *
- * @param providerId
- * @throws UnavailableStorageException
- * If the storage can't be locked.
- */
- public void lockProvider(final String providerId) throws UnavailableStorageException {
- final StorageProvider provider = getProvider(providerId);
- if (provider == null) {
- throw new UnavailableStorageException("StorageProvider not found: " + providerId);
- }
- // lock provider
- final SynchronizationAid sync = mProviderLocks.get(provider);
- final boolean locked = sync.readLock.tryLock();
- if (!locked || (locked && sync.unmounting)) {
- if (locked) {
- sync.readLock.unlock();
- }
- throw new UnavailableStorageException("StorageProvider is unmounting");
- } else if (locked && !provider.isReady(context)) {
- sync.readLock.unlock();
- throw new UnavailableStorageException("StorageProvider not ready");
- }
- }
-
- public void unlockProvider(final String providerId) {
- final StorageProvider provider = getProvider(providerId);
- final SynchronizationAid sync = mProviderLocks.get(provider);
- sync.readLock.unlock();
+ public Set getAvailableProviders() {
+ return mProviders.keySet();
}
}
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/UnavailableStorageException.java b/app/core/src/main/java/com/fsck/k9/mailstore/UnavailableStorageException.java
deleted file mode 100644
index 914add6e69d7ada7310340a2b4554372eb7d099c..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/mailstore/UnavailableStorageException.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.fsck.k9.mailstore;
-
-import com.fsck.k9.mail.MessagingException;
-
-public class UnavailableStorageException extends MessagingException {
-
- private static final long serialVersionUID = 1348267375054620792L;
-
- public UnavailableStorageException(String message) {
- // consider this exception as a permanent failure by default
- this(message, true);
- }
-
- public UnavailableStorageException(String message, boolean perm) {
- super(message, perm);
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/util/FlowedMessageUtils.java b/app/core/src/main/java/com/fsck/k9/mailstore/util/FlowedMessageUtils.java
deleted file mode 100644
index d30665a9251ce731d4cd3d735053a1a6aef8ffc7..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/mailstore/util/FlowedMessageUtils.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.fsck.k9.mailstore.util;
-
-
-/**
- * Adapted from the Apache James project, see
- * https://james.apache.org/mailet/base/apidocs/org/apache/mailet/base/FlowedMessageUtils.html
- *
- * Manages texts encoded as text/plain; format=flowed
.
- * As a reference see:
- *
- * Note
- *
- * - In order to decode, the input text must belong to a mail with headers similar to:
- * Content-Type: text/plain; charset="CHARSET"; [delsp="yes|no"; ]format="flowed"
- * (the quotes around CHARSET are not mandatory).
- * Furthermore the header Content-Transfer-Encoding MUST NOT BE Quoted-Printable
- * (see RFC3676 paragraph 4.2).(In fact this happens often for non 7bit messages).
- *
- *
- */
-public final class FlowedMessageUtils {
- private static final char RFC2646_SPACE = ' ';
- private static final char RFC2646_QUOTE = '>';
- private static final String RFC2646_SIGNATURE = "-- ";
- private static final String RFC2646_CRLF = "\r\n";
-
- private FlowedMessageUtils() {
- // this class cannot be instantiated
- }
-
- /**
- * Decodes a text previously wrapped using "format=flowed".
- */
- public static String deflow(String text, boolean delSp) {
- String[] lines = text.split("\r\n|\n", -1);
- StringBuffer result = null;
- StringBuffer resultLine = new StringBuffer();
- int resultLineQuoteDepth = 0;
- boolean resultLineFlowed = false;
- // One more cycle, to close the last line
- for (int i = 0; i <= lines.length; i++) {
- String line = i < lines.length ? lines[i] : null;
- int actualQuoteDepth = 0;
-
- if (line != null && line.length() > 0) {
- if (line.equals(RFC2646_SIGNATURE))
- // signature handling (the previous line is not flowed)
- resultLineFlowed = false;
-
- else if (line.charAt(0) == RFC2646_QUOTE) {
- // Quote
- actualQuoteDepth = 1;
- while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) actualQuoteDepth ++;
- // if quote-depth changes wrt the previous line then this is not flowed
- if (resultLineQuoteDepth != actualQuoteDepth) resultLineFlowed = false;
- line = line.substring(actualQuoteDepth);
-
- } else {
- // id quote-depth changes wrt the first line then this is not flowed
- if (resultLineQuoteDepth > 0) resultLineFlowed = false;
- }
-
- if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE)
- // Line space-stuffed
- line = line.substring(1);
-
- // if the previous was the last then it was not flowed
- } else if (line == null) resultLineFlowed = false;
-
- // Add the PREVIOUS line.
- // This often will find the flow looking for a space as the last char of the line.
- // With quote changes or signatures it could be the followinf line to void the flow.
- if (!resultLineFlowed && i > 0) {
- if (resultLineQuoteDepth > 0) resultLine.insert(0, RFC2646_SPACE);
- for (int j = 0; j < resultLineQuoteDepth; j++) resultLine.insert(0, RFC2646_QUOTE);
- if (result == null) result = new StringBuffer();
- else result.append(RFC2646_CRLF);
- result.append(resultLine.toString());
- resultLine = new StringBuffer();
- resultLineFlowed = false;
- }
- resultLineQuoteDepth = actualQuoteDepth;
-
- if (line != null) {
- if (!line.equals(RFC2646_SIGNATURE) && line.endsWith("" + RFC2646_SPACE) && i < lines.length - 1) {
- // Line flowed (NOTE: for the split operation the line having i == lines.length is the last that does not end with RFC2646_CRLF)
- if (delSp) line = line.substring(0, line.length() - 1);
- resultLineFlowed = true;
- }
-
- else resultLineFlowed = false;
-
- resultLine.append(line);
- }
- }
-
- return result.toString();
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/message/IdentityHeaderBuilder.java b/app/core/src/main/java/com/fsck/k9/message/IdentityHeaderBuilder.java
index 51eca9da5bd059c91a4a265daa6c0763fbc826da..cb6c2c2344893a694604ba4f19025b07dbfc360a 100644
--- a/app/core/src/main/java/com/fsck/k9/message/IdentityHeaderBuilder.java
+++ b/app/core/src/main/java/com/fsck/k9/message/IdentityHeaderBuilder.java
@@ -3,16 +3,20 @@ package com.fsck.k9.message;
import android.net.Uri;
import android.net.Uri.Builder;
-import timber.log.Timber;
import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.Identity;
+import com.fsck.k9.K9;
import com.fsck.k9.controller.MessageReference;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.message.quote.InsertableHtmlContent;
+import timber.log.Timber;
public class IdentityHeaderBuilder {
+ private static final int MAX_LINE_LENGTH = 72;
+ private static final int FIRST_LINE_EXTRA_LENGTH = K9.IDENTITY_HEADER.length() + 2;
+
private InsertableHtmlContent quotedHtmlContent;
private QuoteStyle quoteStyle;
private SimpleMessageFormat messageFormat;
@@ -95,9 +99,33 @@ public class IdentityHeaderBuilder {
appendValue(IdentityField.QUOTED_TEXT_MODE, quotedTextMode);
String k9identity = IdentityField.IDENTITY_VERSION_1 + uri.build().getEncodedQuery();
+ String headerValue = foldHeaderValue(k9identity);
+
+ Timber.d("Generated identity: %s", headerValue);
+ return headerValue;
+ }
+
+ private String foldHeaderValue(String input) {
+ int inputLength = input.length();
+ int endOfFirstLine = MAX_LINE_LENGTH - FIRST_LINE_EXTRA_LENGTH;
+ if (inputLength <= endOfFirstLine) {
+ return input;
+ }
+
+ int extraLines = (inputLength - endOfFirstLine - 1) / (MAX_LINE_LENGTH - 1) + 1;
+ int builderSize = inputLength + extraLines * 3 /* CR LF SPACE */;
+ StringBuilder headerValue = new StringBuilder(builderSize);
+
+ headerValue.append(input, 0, endOfFirstLine);
+ int start = endOfFirstLine;
+ while (start < inputLength) {
+ headerValue.append("\r\n ");
+ int end = start + Math.min(MAX_LINE_LENGTH - 1, inputLength - start);
+ headerValue.append(input, start, end);
+ start = end;
+ }
- Timber.d("Generated identity: %s", k9identity);
- return k9identity;
+ return headerValue.toString();
}
private void appendValue(IdentityField field, int value) {
diff --git a/app/core/src/main/java/com/fsck/k9/message/IdentityHeaderParser.java b/app/core/src/main/java/com/fsck/k9/message/IdentityHeaderParser.java
index e73cea944de41cee98ddf8b84b3a35639860724a..14b97add02bb686523ae69f39ee46778f0d7df93 100644
--- a/app/core/src/main/java/com/fsck/k9/message/IdentityHeaderParser.java
+++ b/app/core/src/main/java/com/fsck/k9/message/IdentityHeaderParser.java
@@ -29,10 +29,12 @@ public class IdentityHeaderParser {
return identity;
}
+ String encodedString = unfoldHeaderValue(identityString);
+
// Check to see if this is a "next gen" identity.
- if (identityString.charAt(0) == IdentityField.IDENTITY_VERSION_1.charAt(0) && identityString.length() > 2) {
+ if (encodedString.charAt(0) == IdentityField.IDENTITY_VERSION_1.charAt(0) && encodedString.length() > 2) {
Uri.Builder builder = new Uri.Builder();
- builder.encodedQuery(identityString.substring(1)); // Need to cut off the ! at the beginning.
+ builder.encodedQuery(encodedString.substring(1)); // Need to cut off the ! at the beginning.
Uri uri = builder.build();
for (IdentityField key : IdentityField.values()) {
String value = uri.getQueryParameter(key.value());
@@ -56,9 +58,9 @@ public class IdentityHeaderParser {
} else {
// Legacy identity
- Timber.d("Got a saved legacy identity: %s", identityString);
+ Timber.d("Got a saved legacy identity: %s", encodedString);
- StringTokenizer tokenizer = new StringTokenizer(identityString, ":", false);
+ StringTokenizer tokenizer = new StringTokenizer(encodedString, ":", false);
// First item is the body length. We use this to separate the composed reply from the quoted text.
if (tokenizer.hasMoreTokens()) {
@@ -85,4 +87,8 @@ public class IdentityHeaderParser {
return identity;
}
+
+ private static String unfoldHeaderValue(String identityString) {
+ return identityString.replaceAll("\r?\n ", "");
+ }
}
diff --git a/app/core/src/main/java/com/fsck/k9/message/MessageBuilder.java b/app/core/src/main/java/com/fsck/k9/message/MessageBuilder.java
index 8ea190b154ef59350523fef7600bf269153a1b5f..0df05d2c8ec491de36943c47c769365a94e38f07 100644
--- a/app/core/src/main/java/com/fsck/k9/message/MessageBuilder.java
+++ b/app/core/src/main/java/com/fsck/k9/message/MessageBuilder.java
@@ -50,6 +50,7 @@ public abstract class MessageBuilder {
private Address[] to;
private Address[] cc;
private Address[] bcc;
+ private Address[] replyTo;
private String inReplyTo;
private String references;
private boolean requestReadReceipt;
@@ -115,10 +116,7 @@ public abstract class MessageBuilder {
message.setHeader("User-Agent", encodedUserAgent);
}
- final String replyTo = identity.getReplyTo();
- if (replyTo != null) {
- message.setReplyTo(new Address[] { new Address(replyTo) });
- }
+ message.setReplyTo(replyTo);
if (inReplyTo != null) {
message.setInReplyTo(inReplyTo);
@@ -395,6 +393,11 @@ public abstract class MessageBuilder {
return this;
}
+ public MessageBuilder setReplyTo(Address[] replyTo) {
+ this.replyTo = replyTo;
+ return this;
+ }
+
public MessageBuilder setInReplyTo(String inReplyTo) {
this.inReplyTo = inReplyTo;
return this;
diff --git a/app/core/src/main/java/com/fsck/k9/message/extractors/BasicPartInfoExtractor.kt b/app/core/src/main/java/com/fsck/k9/message/extractors/BasicPartInfoExtractor.kt
index b571abece8dd17612a542f997e462679e9f941ed..6992a87a4a1bd344786e1e913afb5fd74f76f898 100644
--- a/app/core/src/main/java/com/fsck/k9/message/extractors/BasicPartInfoExtractor.kt
+++ b/app/core/src/main/java/com/fsck/k9/message/extractors/BasicPartInfoExtractor.kt
@@ -4,7 +4,6 @@ import com.fsck.k9.mail.Part
import com.fsck.k9.mail.internet.MimeParameterDecoder
import com.fsck.k9.mail.internet.MimeUtility
import com.fsck.k9.mail.internet.MimeValue
-import java.util.Locale
private const val FALLBACK_NAME = "noname"
@@ -38,11 +37,11 @@ class BasicPartInfoExtractor {
private fun String.toMimeValue(): MimeValue = MimeParameterDecoder.decode(this)
- private fun MimeValue.getParameter(name: String): String? = parameters[name.toLowerCase(Locale.ROOT)]
+ private fun MimeValue.getParameter(name: String): String? = parameters[name.lowercase()]
private fun String.getParameter(name: String): String? {
val mimeValue = MimeParameterDecoder.decode(this)
- return mimeValue.parameters[name.toLowerCase(Locale.ROOT)]
+ return mimeValue.parameters[name.lowercase()]
}
}
diff --git a/app/core/src/main/java/com/fsck/k9/message/html/HtmlToPlainText.kt b/app/core/src/main/java/com/fsck/k9/message/html/HtmlToPlainText.kt
index 191270718c05c1cb2520e76985861ba88b8900fb..9ea06eafe74b8c8152699a19d0ec6144bc15820d 100644
--- a/app/core/src/main/java/com/fsck/k9/message/html/HtmlToPlainText.kt
+++ b/app/core/src/main/java/com/fsck/k9/message/html/HtmlToPlainText.kt
@@ -22,7 +22,6 @@ object HtmlToPlainText {
}
private class FormattingVisitor : NodeVisitor {
- private var width = 0
private val output = StringBuilder()
private var collectLinkText = false
private var linkText = StringBuilder()
@@ -73,36 +72,11 @@ private class FormattingVisitor : NodeVisitor {
}
private fun append(text: String) {
- if (text.startsWith("\n")) {
- width = 0
- }
-
if (text == " " && (output.isEmpty() || output.last() in listOf(' ', '\n'))) {
return
}
- if (text.length + width > MAX_WIDTH) {
- val words = text.split(Regex("\\s+"))
- for (i in words.indices) {
- var word = words[i]
-
- val last = i == words.size - 1
- if (!last) {
- word = "$word "
- }
-
- if (word.length + width > MAX_WIDTH) {
- output.append("\n").append(word)
- width = word.length
- } else {
- output.append(word)
- width += word.length
- }
- }
- } else {
- output.append(text)
- width += text.length
- }
+ output.append(text)
}
private fun startNewLine() {
@@ -134,8 +108,4 @@ private class FormattingVisitor : NodeVisitor {
return output.substring(0, lastIndex + 1)
}
-
- companion object {
- private const val MAX_WIDTH = 76
- }
}
diff --git a/app/core/src/main/java/com/fsck/k9/message/html/UriMatcher.kt b/app/core/src/main/java/com/fsck/k9/message/html/UriMatcher.kt
index ee5b44344d3191b2e19b5ef45143b01b1c22c8b8..5dea02560e4928e60caeffd8a5930f8d3755fbc6 100644
--- a/app/core/src/main/java/com/fsck/k9/message/html/UriMatcher.kt
+++ b/app/core/src/main/java/com/fsck/k9/message/html/UriMatcher.kt
@@ -1,9 +1,8 @@
package com.fsck.k9.message.html
-import java.util.Locale
-
object UriMatcher {
- private val SUPPORTED_URIS = { httpUriParser: HttpUriParser ->
+ private val SUPPORTED_URIS = run {
+ val httpUriParser = HttpUriParser()
mapOf(
"ethereum:" to EthereumUriParser(),
"bitcoin:" to BitcoinUriParser(),
@@ -11,7 +10,7 @@ object UriMatcher {
"https:" to httpUriParser,
"rtsp:" to httpUriParser
)
- }.invoke(HttpUriParser())
+ }
private const val SCHEME_SEPARATORS = "\\s(\\n<"
private const val ALLOWED_SEPARATORS_PATTERN = "(?:^|[$SCHEME_SEPARATORS])"
@@ -23,8 +22,8 @@ object UriMatcher {
fun findUris(text: CharSequence): List {
return URI_SCHEME.findAll(text).map { matchResult ->
val matchGroup = matchResult.groups[1]!!
- val startIndex = matchGroup.range.start
- val scheme = matchGroup.value.toLowerCase(Locale.ROOT)
+ val startIndex = matchGroup.range.first
+ val scheme = matchGroup.value.lowercase()
val parser = SUPPORTED_URIS[scheme] ?: throw AssertionError("Scheme not found: $scheme")
parser.parseUri(text, startIndex)
diff --git a/app/core/src/main/java/com/fsck/k9/notification/AddNotificationResult.java b/app/core/src/main/java/com/fsck/k9/notification/AddNotificationResult.java
deleted file mode 100644
index 9dd3257fa1a025497a71cddda1fa9a5370c332db..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/AddNotificationResult.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.fsck.k9.notification;
-
-
-class AddNotificationResult {
- private final NotificationHolder notificationHolder;
- private final boolean cancelNotificationBeforeReuse;
-
-
- private AddNotificationResult(NotificationHolder notificationHolder,
- boolean cancelNotificationBeforeReuse) {
- this.notificationHolder = notificationHolder;
- this.cancelNotificationBeforeReuse = cancelNotificationBeforeReuse;
- }
-
- public static AddNotificationResult newNotification(NotificationHolder notificationHolder) {
- return new AddNotificationResult(notificationHolder, false);
- }
-
- public static AddNotificationResult replaceNotification(NotificationHolder notificationHolder) {
- return new AddNotificationResult(notificationHolder, true);
- }
-
- public boolean shouldCancelNotification() {
- return cancelNotificationBeforeReuse;
- }
-
- public int getNotificationId() {
- if (!shouldCancelNotification()) {
- throw new IllegalStateException("getNotificationId() can only be called when " +
- "shouldCancelNotification() returns true");
- }
-
- return notificationHolder.notificationId;
- }
-
- public NotificationHolder getNotificationHolder() {
- return notificationHolder;
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/AddNotificationResult.kt b/app/core/src/main/java/com/fsck/k9/notification/AddNotificationResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..662235eb2f9714bd308c58eab09b0a426f15992a
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/AddNotificationResult.kt
@@ -0,0 +1,42 @@
+package com.fsck.k9.notification
+
+internal class AddNotificationResult private constructor(
+ val notificationData: NotificationData,
+ val notificationStoreOperations: List,
+ val notificationHolder: NotificationHolder,
+ val shouldCancelNotification: Boolean
+) {
+ val cancelNotificationId: Int
+ get() {
+ check(shouldCancelNotification) { "shouldCancelNotification == false" }
+ return notificationHolder.notificationId
+ }
+
+ companion object {
+ fun newNotification(
+ notificationData: NotificationData,
+ notificationStoreOperations: List,
+ notificationHolder: NotificationHolder
+ ): AddNotificationResult {
+ return AddNotificationResult(
+ notificationData,
+ notificationStoreOperations,
+ notificationHolder,
+ shouldCancelNotification = false
+ )
+ }
+
+ fun replaceNotification(
+ notificationData: NotificationData,
+ notificationStoreOperations: List,
+ notificationHolder: NotificationHolder
+ ): AddNotificationResult {
+ return AddNotificationResult(
+ notificationData,
+ notificationStoreOperations,
+ notificationHolder,
+ shouldCancelNotification = true
+ )
+ }
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/AuthenticationErrorNotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/AuthenticationErrorNotificationController.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fad5368c95f9e1e9a72b5a6465592653274fed70
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/AuthenticationErrorNotificationController.kt
@@ -0,0 +1,71 @@
+package com.fsck.k9.notification
+
+import android.app.Notification
+import android.app.PendingIntent
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import com.fsck.k9.Account
+
+internal open class AuthenticationErrorNotificationController(
+ private val notificationHelper: NotificationHelper,
+ private val actionCreator: NotificationActionCreator,
+ private val resourceProvider: NotificationResourceProvider
+) {
+ fun showAuthenticationErrorNotification(account: Account, incoming: Boolean) {
+ val notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, incoming)
+ val editServerSettingsPendingIntent = createContentIntent(account, incoming)
+ val title = resourceProvider.authenticationErrorTitle()
+ val text = resourceProvider.authenticationErrorBody(account.description)
+
+ val notificationBuilder = notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconWarning)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setAutoCancel(true)
+ .setTicker(title)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(editServerSettingsPendingIntent)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(text))
+ .setPublicVersion(createLockScreenNotification(account))
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+
+ notificationHelper.configureNotification(
+ builder = notificationBuilder,
+ ringtone = null,
+ vibrationPattern = null,
+ ledColor = NotificationHelper.NOTIFICATION_LED_FAILURE_COLOR,
+ ledSpeed = NotificationHelper.NOTIFICATION_LED_BLINK_FAST,
+ ringAndVibrate = true
+ )
+
+ notificationManager.notify(notificationId, notificationBuilder.build())
+ }
+
+ fun clearAuthenticationErrorNotification(account: Account, incoming: Boolean) {
+ val notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, incoming)
+ notificationManager.cancel(notificationId)
+ }
+
+ protected open fun createContentIntent(account: Account, incoming: Boolean): PendingIntent {
+ return if (incoming) {
+ actionCreator.getEditIncomingServerSettingsIntent(account)
+ } else {
+ actionCreator.getEditOutgoingServerSettingsIntent(account)
+ }
+ }
+
+ private fun createLockScreenNotification(account: Account): Notification {
+ return notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconWarning)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle(resourceProvider.authenticationErrorTitle())
+ .build()
+ }
+
+ private val notificationManager: NotificationManagerCompat
+ get() = notificationHelper.getNotificationManager()
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/AuthenticationErrorNotifications.java b/app/core/src/main/java/com/fsck/k9/notification/AuthenticationErrorNotifications.java
deleted file mode 100644
index 302199d4da6f021ebc595f5c0d0fc50746addca5..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/AuthenticationErrorNotifications.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.PendingIntent;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.BigTextStyle;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.fsck.k9.Account;
-
-import static com.fsck.k9.notification.NotificationHelper.NOTIFICATION_LED_BLINK_FAST;
-import static com.fsck.k9.notification.NotificationHelper.NOTIFICATION_LED_FAILURE_COLOR;
-
-
-class AuthenticationErrorNotifications {
- private final NotificationHelper notificationHelper;
- private final NotificationActionCreator actionCreator;
- private final NotificationResourceProvider resourceProvider;
-
-
- public AuthenticationErrorNotifications(NotificationHelper notificationHelper,
- NotificationActionCreator actionCreator, NotificationResourceProvider resourceProvider) {
- this.notificationHelper = notificationHelper;
- this.actionCreator = actionCreator;
- this.resourceProvider = resourceProvider;
- }
-
- public void showAuthenticationErrorNotification(Account account, boolean incoming) {
- int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, incoming);
-
- PendingIntent editServerSettingsPendingIntent = createContentIntent(account, incoming);
- String title = resourceProvider.authenticationErrorTitle();
- String text = resourceProvider.authenticationErrorBody(account.getDescription());
-
- NotificationCompat.Builder builder = notificationHelper
- .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
- .setSmallIcon(resourceProvider.getIconWarning())
- .setWhen(System.currentTimeMillis())
- .setAutoCancel(true)
- .setTicker(title)
- .setContentTitle(title)
- .setContentText(text)
- .setContentIntent(editServerSettingsPendingIntent)
- .setStyle(new BigTextStyle().bigText(text))
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setCategory(NotificationCompat.CATEGORY_ERROR);
-
- notificationHelper.configureNotification(builder, null, null,
- NOTIFICATION_LED_FAILURE_COLOR,
- NOTIFICATION_LED_BLINK_FAST, true);
-
- getNotificationManager().notify(notificationId, builder.build());
- }
-
- public void clearAuthenticationErrorNotification(Account account, boolean incoming) {
- int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, incoming);
- getNotificationManager().cancel(notificationId);
- }
-
- PendingIntent createContentIntent(Account account, boolean incoming) {
- return incoming ? actionCreator.getEditIncomingServerSettingsIntent(account) :
- actionCreator.getEditOutgoingServerSettingsIntent(account);
- }
-
- private NotificationManagerCompat getNotificationManager() {
- return notificationHelper.getNotificationManager();
- }
-}
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
new file mode 100644
index 0000000000000000000000000000000000000000..936f884c4dd10dee9d0a579e8b1d13e3e890412e
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/BaseNotificationDataCreator.kt
@@ -0,0 +1,53 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.K9
+import com.fsck.k9.K9.LockScreenNotificationVisibility
+
+private const val MAX_NUMBER_OF_SENDERS_IN_LOCK_SCREEN_NOTIFICATION = 5
+
+internal class BaseNotificationDataCreator {
+
+ fun createBaseNotificationData(notificationData: NotificationData): BaseNotificationData {
+ val account = notificationData.account
+ return BaseNotificationData(
+ account = account,
+ groupKey = NotificationGroupKeys.getGroupKey(account),
+ accountName = getAccountName(account),
+ color = account.chipColor,
+ newMessagesCount = notificationData.newMessagesCount,
+ lockScreenNotificationData = createLockScreenNotificationData(notificationData),
+ appearance = createNotificationAppearance(account)
+ )
+ }
+
+ private fun getAccountName(account: Account): String {
+ val accountDescription = account.description?.takeIf { it.isNotEmpty() }
+ return accountDescription ?: account.email
+ }
+
+ private fun createLockScreenNotificationData(data: NotificationData): LockScreenNotificationData {
+ return when (K9.lockScreenNotificationVisibility) {
+ LockScreenNotificationVisibility.NOTHING -> LockScreenNotificationData.None
+ LockScreenNotificationVisibility.APP_NAME -> LockScreenNotificationData.AppName
+ LockScreenNotificationVisibility.EVERYTHING -> LockScreenNotificationData.Public
+ LockScreenNotificationVisibility.MESSAGE_COUNT -> LockScreenNotificationData.MessageCount
+ LockScreenNotificationVisibility.SENDERS -> LockScreenNotificationData.SenderNames(getSenderNames(data))
+ }
+ }
+
+ private fun getSenderNames(data: NotificationData): String {
+ return data.activeNotifications.asSequence()
+ .map { it.content.sender }
+ .distinct()
+ .take(MAX_NUMBER_OF_SENDERS_IN_LOCK_SCREEN_NOTIFICATION)
+ .joinToString()
+ }
+
+ private fun createNotificationAppearance(account: Account): NotificationAppearance {
+ return with(account.notificationSetting) {
+ val vibrationPattern = if (isVibrateEnabled) vibration else null
+ NotificationAppearance(ringtone, vibrationPattern, ledColor)
+ }
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/BaseNotifications.java b/app/core/src/main/java/com/fsck/k9/notification/BaseNotifications.java
deleted file mode 100644
index 5534372de0085aaf11e07894dc1671764cd04007..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/BaseNotifications.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.PendingIntent;
-import android.content.Context;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.BigTextStyle;
-import androidx.core.app.NotificationCompat.Builder;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.K9.NotificationQuickDelete;
-
-
-abstract class BaseNotifications {
- protected final Context context;
- protected final NotificationHelper notificationHelper;
- protected final NotificationActionCreator actionCreator;
- protected final NotificationResourceProvider resourceProvider;
-
-
- protected BaseNotifications(NotificationHelper notificationHelper, NotificationActionCreator actionCreator,
- NotificationResourceProvider resourceProvider) {
- this.context = notificationHelper.getContext();
- this.notificationHelper = notificationHelper;
- this.actionCreator = actionCreator;
- this.resourceProvider = resourceProvider;
- }
-
- protected NotificationCompat.Builder createBigTextStyleNotification(Account account, NotificationHolder holder,
- int notificationId) {
- String accountName = notificationHelper.getAccountName(account);
- NotificationContent content = holder.content;
- String groupKey = NotificationGroupKeys.getGroupKey(account);
-
- NotificationCompat.Builder builder = createAndInitializeNotificationBuilder(account)
- .setTicker(content.summary)
- .setGroup(groupKey)
- .setContentTitle(content.sender)
- .setContentText(content.subject)
- .setSubText(accountName);
-
- NotificationCompat.BigTextStyle style = createBigTextStyle(builder);
- style.bigText(content.preview);
-
- builder.setStyle(style);
-
- PendingIntent contentIntent = actionCreator.createViewMessagePendingIntent(
- content.messageReference, notificationId);
- builder.setContentIntent(contentIntent);
-
- return builder;
- }
-
- protected NotificationCompat.Builder createAndInitializeNotificationBuilder(Account account) {
- return notificationHelper.createNotificationBuilder(account,
- NotificationChannelManager.ChannelType.MESSAGES)
- .setSmallIcon(getNewMailNotificationIcon())
- .setColor(account.getChipColor())
- .setWhen(System.currentTimeMillis())
- .setAutoCancel(true)
- .setCategory(NotificationCompat.CATEGORY_EMAIL);
- }
-
- protected boolean isDeleteActionEnabled() {
- NotificationQuickDelete deleteOption = K9.getNotificationQuickDeleteBehaviour();
- return deleteOption == NotificationQuickDelete.ALWAYS || deleteOption == NotificationQuickDelete.FOR_SINGLE_MSG;
- }
-
- protected BigTextStyle createBigTextStyle(Builder builder) {
- return new BigTextStyle(builder);
- }
-
- private int getNewMailNotificationIcon() {
- return resourceProvider.getIconNewMail();
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/CertificateErrorNotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/CertificateErrorNotificationController.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e787b85efbaa09c9ec7da7424dc48438599b138b
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/CertificateErrorNotificationController.kt
@@ -0,0 +1,71 @@
+package com.fsck.k9.notification
+
+import android.app.Notification
+import android.app.PendingIntent
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import com.fsck.k9.Account
+
+internal open class CertificateErrorNotificationController(
+ private val notificationHelper: NotificationHelper,
+ private val actionCreator: NotificationActionCreator,
+ private val resourceProvider: NotificationResourceProvider
+) {
+ fun showCertificateErrorNotification(account: Account, incoming: Boolean) {
+ val notificationId = NotificationIds.getCertificateErrorNotificationId(account, incoming)
+ val editServerSettingsPendingIntent = createContentIntent(account, incoming)
+ val title = resourceProvider.certificateErrorTitle(account.description)
+ val text = resourceProvider.certificateErrorBody()
+
+ val notificationBuilder = notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconWarning)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setAutoCancel(true)
+ .setTicker(title)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(editServerSettingsPendingIntent)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(text))
+ .setPublicVersion(createLockScreenNotification(account))
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+
+ notificationHelper.configureNotification(
+ builder = notificationBuilder,
+ ringtone = null,
+ vibrationPattern = null,
+ ledColor = NotificationHelper.NOTIFICATION_LED_FAILURE_COLOR,
+ ledSpeed = NotificationHelper.NOTIFICATION_LED_BLINK_FAST,
+ ringAndVibrate = true
+ )
+
+ notificationManager.notify(notificationId, notificationBuilder.build())
+ }
+
+ fun clearCertificateErrorNotifications(account: Account, incoming: Boolean) {
+ val notificationId = NotificationIds.getCertificateErrorNotificationId(account, incoming)
+ notificationManager.cancel(notificationId)
+ }
+
+ protected open fun createContentIntent(account: Account, incoming: Boolean): PendingIntent {
+ return if (incoming) {
+ actionCreator.getEditIncomingServerSettingsIntent(account)
+ } else {
+ actionCreator.getEditOutgoingServerSettingsIntent(account)
+ }
+ }
+
+ private fun createLockScreenNotification(account: Account): Notification {
+ return notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconWarning)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle(resourceProvider.certificateErrorTitle())
+ .build()
+ }
+
+ private val notificationManager: NotificationManagerCompat
+ get() = notificationHelper.getNotificationManager()
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/CertificateErrorNotifications.java b/app/core/src/main/java/com/fsck/k9/notification/CertificateErrorNotifications.java
deleted file mode 100644
index a5239b1634abfc8f311048c0ad162083ef344243..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/CertificateErrorNotifications.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.PendingIntent;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.BigTextStyle;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.fsck.k9.Account;
-
-import static com.fsck.k9.notification.NotificationHelper.NOTIFICATION_LED_BLINK_FAST;
-import static com.fsck.k9.notification.NotificationHelper.NOTIFICATION_LED_FAILURE_COLOR;
-
-
-class CertificateErrorNotifications {
- private final NotificationHelper notificationHelper;
- private final NotificationActionCreator actionCreator;
- private final NotificationResourceProvider resourceProvider;
-
-
- public CertificateErrorNotifications(NotificationHelper notificationHelper,
- NotificationActionCreator actionCreator, NotificationResourceProvider resourceProvider) {
- this.notificationHelper = notificationHelper;
- this.actionCreator = actionCreator;
- this.resourceProvider = resourceProvider;
- }
-
- public void showCertificateErrorNotification(Account account, boolean incoming) {
- int notificationId = NotificationIds.getCertificateErrorNotificationId(account, incoming);
-
- PendingIntent editServerSettingsPendingIntent = createContentIntent(account, incoming);
- String title = resourceProvider.certificateErrorTitle(account.getDescription());
- String text = resourceProvider.certificateErrorBody();
-
- NotificationCompat.Builder builder = notificationHelper
- .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
- .setSmallIcon(resourceProvider.getIconWarning())
- .setWhen(System.currentTimeMillis())
- .setAutoCancel(true)
- .setTicker(title)
- .setContentTitle(title)
- .setContentText(text)
- .setContentIntent(editServerSettingsPendingIntent)
- .setStyle(new BigTextStyle().bigText(text))
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setCategory(NotificationCompat.CATEGORY_ERROR);
-
- notificationHelper.configureNotification(builder, null, null,
- NOTIFICATION_LED_FAILURE_COLOR,
- NOTIFICATION_LED_BLINK_FAST, true);
-
- getNotificationManager().notify(notificationId, builder.build());
- }
-
- public void clearCertificateErrorNotifications(Account account, boolean incoming) {
- int notificationId = NotificationIds.getCertificateErrorNotificationId(account, incoming);
- getNotificationManager().cancel(notificationId);
- }
-
- PendingIntent createContentIntent(Account account, boolean incoming) {
- return incoming ?
- actionCreator.getEditIncomingServerSettingsIntent(account) :
- actionCreator.getEditOutgoingServerSettingsIntent(account);
- }
-
- private NotificationManagerCompat getNotificationManager() {
- return notificationHelper.getNotificationManager();
- }
-}
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 6904c91255921af2b0857da4dc647a9d04091690..dba15df01c6a5cf718223dd1e12c7479fe2227bf 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
@@ -8,15 +8,23 @@ import java.util.concurrent.Executors
import org.koin.dsl.module
val coreNotificationModule = module {
- single { NotificationController(get(), get(), get(), get(), get()) }
+ single {
+ NotificationController(
+ certificateErrorNotificationController = get(),
+ authenticationErrorNotificationController = get(),
+ syncNotificationController = get(),
+ sendFailedNotificationController = get(),
+ newMailNotificationController = get()
+ )
+ }
single { NotificationManagerCompat.from(get()) }
- single { NotificationHelper(get(), get(), get()) }
+ single { NotificationHelper(context = get(), notificationManager = get(), channelUtils = get()) }
single {
NotificationChannelManager(
- get(),
- Executors.newSingleThreadExecutor(),
- get().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager,
- get()
+ preferences = get(),
+ backgroundExecutor = Executors.newSingleThreadExecutor(),
+ notificationManager = get().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager,
+ resourceProvider = get()
)
}
single {
@@ -26,15 +34,68 @@ val coreNotificationModule = module {
serverSettingsSerializer = get()
)
}
- single { CertificateErrorNotifications(get(), get(), get()) }
- single { AuthenticationErrorNotifications(get(), get(), get()) }
- single { SyncNotifications(get(), get(), get()) }
- single { SendFailedNotifications(get(), get(), get()) }
- single { NewMailNotifications(get(), get(), get(), get()) }
- single { NotificationContentCreator(get(), get()) }
- single { WearNotifications(get(), get(), get()) }
- single { DeviceNotifications(get(), get(), get(), get(), get()) }
- single { LockScreenNotification(get(), get()) }
+ single {
+ CertificateErrorNotificationController(
+ notificationHelper = get(),
+ actionCreator = get(),
+ resourceProvider = get()
+ )
+ }
+ single {
+ AuthenticationErrorNotificationController(
+ notificationHelper = get(),
+ actionCreator = get(),
+ resourceProvider = get()
+ )
+ }
+ single {
+ SyncNotificationController(notificationHelper = get(), actionBuilder = get(), resourceProvider = get())
+ }
+ single {
+ SendFailedNotificationController(notificationHelper = get(), actionBuilder = get(), resourceProvider = get())
+ }
+ single {
+ NewMailNotificationController(
+ notificationManager = get(),
+ newMailNotificationManager = get(),
+ summaryNotificationCreator = get(),
+ singleMessageNotificationCreator = get()
+ )
+ }
+ single {
+ NewMailNotificationManager(
+ contentCreator = get(),
+ notificationRepository = get(),
+ baseNotificationDataCreator = get(),
+ singleMessageNotificationDataCreator = get(),
+ summaryNotificationDataCreator = get(),
+ clock = get()
+ )
+ }
+ factory { NotificationContentCreator(context = get(), resourceProvider = get()) }
+ factory { BaseNotificationDataCreator() }
+ factory { SingleMessageNotificationDataCreator() }
+ factory { SummaryNotificationDataCreator(singleMessageNotificationDataCreator = get()) }
+ factory {
+ SingleMessageNotificationCreator(
+ notificationHelper = get(),
+ actionCreator = get(),
+ resourceProvider = get(),
+ lockScreenNotificationCreator = get(),
+ notificationManager = get()
+ )
+ }
+ factory {
+ SummaryNotificationCreator(
+ notificationHelper = get(),
+ actionCreator = get(),
+ lockScreenNotificationCreator = get(),
+ singleMessageNotificationCreator = get(),
+ resourceProvider = get(),
+ notificationManager = get()
+ )
+ }
+ factory { LockScreenNotificationCreator(notificationHelper = get(), resourceProvider = get()) }
single {
PushNotificationManager(
context = get(),
@@ -43,4 +104,12 @@ val coreNotificationModule = module {
notificationManager = get()
)
}
+ single {
+ NotificationRepository(
+ notificationStoreProvider = get(),
+ localStoreProvider = get(),
+ messageStoreManager = get(),
+ notificationContentCreator = get()
+ )
+ }
}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/DeviceNotifications.java b/app/core/src/main/java/com/fsck/k9/notification/DeviceNotifications.java
deleted file mode 100644
index d9b7af54b01e33e2193c4eb287c66b03d84ba5a7..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/DeviceNotifications.java
+++ /dev/null
@@ -1,235 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.ArrayList;
-import java.util.List;
-
-import android.app.KeyguardManager;
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.Context;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.Builder;
-import androidx.core.app.NotificationCompat.InboxStyle;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.K9.NotificationHideSubject;
-import com.fsck.k9.K9.NotificationQuickDelete;
-import com.fsck.k9.NotificationSetting;
-import com.fsck.k9.controller.MessageReference;
-
-import static com.fsck.k9.notification.NotificationHelper.NOTIFICATION_LED_BLINK_SLOW;
-
-
-class DeviceNotifications extends BaseNotifications {
- private final WearNotifications wearNotifications;
- private final LockScreenNotification lockScreenNotification;
-
-
- DeviceNotifications(NotificationHelper notificationHelper, NotificationActionCreator actionCreator,
- LockScreenNotification lockScreenNotification, WearNotifications wearNotifications,
- NotificationResourceProvider resourceProvider) {
- super(notificationHelper, actionCreator, resourceProvider);
- this.wearNotifications = wearNotifications;
- this.lockScreenNotification = lockScreenNotification;
- }
-
- public Notification buildSummaryNotification(Account account, NotificationData notificationData,
- boolean silent) {
- int unreadMessageCount = notificationData.getUnreadMessageCount();
-
- NotificationCompat.Builder builder;
- if (isPrivacyModeActive()) {
- builder = createSimpleSummaryNotification(account, unreadMessageCount);
- } else if (notificationData.isSingleMessageNotification()) {
- NotificationHolder holder = notificationData.getHolderForLatestNotification();
- builder = createBigTextStyleSummaryNotification(account, holder);
- } else {
- builder = createInboxStyleSummaryNotification(account, notificationData, unreadMessageCount);
- }
-
- if (notificationData.containsStarredMessages()) {
- builder.setPriority(NotificationCompat.PRIORITY_HIGH);
- }
-
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- PendingIntent deletePendingIntent = actionCreator.createDismissAllMessagesPendingIntent(
- account, notificationId);
- builder.setDeleteIntent(deletePendingIntent);
-
- lockScreenNotification.configureLockScreenNotification(builder, notificationData);
-
- boolean ringAndVibrate = false;
- if (!silent && !account.isRingNotified()) {
- account.setRingNotified(true);
- ringAndVibrate = true;
- }
-
- NotificationSetting notificationSetting = account.getNotificationSetting();
- notificationHelper.configureNotification(
- builder,
- (notificationSetting.isRingEnabled()) ? notificationSetting.getRingtone() : null,
- (notificationSetting.isVibrateEnabled()) ? notificationSetting.getVibration() : null,
- (notificationSetting.isLedEnabled()) ? notificationSetting.getLedColor() : null,
- NOTIFICATION_LED_BLINK_SLOW,
- ringAndVibrate);
-
- return builder.build();
- }
-
- private NotificationCompat.Builder createSimpleSummaryNotification(Account account, int unreadMessageCount) {
- String accountName = notificationHelper.getAccountName(account);
- CharSequence newMailText = resourceProvider.newMailTitle();
- String unreadMessageCountText = resourceProvider.newMailUnreadMessageCount(unreadMessageCount, accountName);
-
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- PendingIntent contentIntent = actionCreator.createViewFolderListPendingIntent(account, notificationId);
-
- return createAndInitializeNotificationBuilder(account)
- .setNumber(unreadMessageCount)
- .setTicker(newMailText)
- .setContentTitle(unreadMessageCountText)
- .setContentText(newMailText)
- .setContentIntent(contentIntent);
- }
-
- private NotificationCompat.Builder createBigTextStyleSummaryNotification(Account account,
- NotificationHolder holder) {
-
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- Builder builder = createBigTextStyleNotification(account, holder, notificationId)
- .setGroupSummary(true);
-
- NotificationContent content = holder.content;
- addReplyAction(builder, content, notificationId);
- addMarkAsReadAction(builder, content, notificationId);
- addDeleteAction(builder, content, notificationId);
-
- return builder;
- }
-
- private NotificationCompat.Builder createInboxStyleSummaryNotification(Account account,
- NotificationData notificationData, int unreadMessageCount) {
-
- NotificationHolder latestNotification = notificationData.getHolderForLatestNotification();
-
- int newMessagesCount = notificationData.getNewMessagesCount();
- String accountName = notificationHelper.getAccountName(account);
- String title = resourceProvider.newMessagesTitle(newMessagesCount);
- String summary = (notificationData.hasSummaryOverflowMessages()) ?
- resourceProvider.additionalMessages(notificationData.getSummaryOverflowMessagesCount(), accountName) :
- accountName;
- String groupKey = NotificationGroupKeys.getGroupKey(account);
-
- NotificationCompat.Builder builder = createAndInitializeNotificationBuilder(account)
- .setNumber(unreadMessageCount)
- .setTicker(latestNotification.content.summary)
- .setGroup(groupKey)
- .setGroupSummary(true)
- .setContentTitle(title)
- .setSubText(accountName);
-
- NotificationCompat.InboxStyle style = createInboxStyle(builder)
- .setBigContentTitle(title)
- .setSummaryText(summary);
-
- for (NotificationContent content : notificationData.getContentForSummaryNotification()) {
- style.addLine(content.summary);
- }
-
- builder.setStyle(style);
-
- addMarkAllAsReadAction(builder, notificationData);
- addDeleteAllAction(builder, notificationData);
-
- wearNotifications.addSummaryActions(builder, notificationData);
-
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- List messageReferences = notificationData.getAllMessageReferences();
- PendingIntent contentIntent = actionCreator.createViewMessagesPendingIntent(
- account, messageReferences, notificationId);
- builder.setContentIntent(contentIntent);
-
- return builder;
- }
-
- private void addMarkAsReadAction(Builder builder, NotificationContent content, int notificationId) {
- int icon = resourceProvider.getIconMarkAsRead();
- String title = resourceProvider.actionMarkAsRead();
-
-
- MessageReference messageReference = content.messageReference;
- PendingIntent action = actionCreator.createMarkMessageAsReadPendingIntent(messageReference, notificationId);
-
- builder.addAction(icon, title, action);
- }
-
- private void addMarkAllAsReadAction(Builder builder, NotificationData notificationData) {
- int icon = resourceProvider.getIconMarkAsRead();
- String title = resourceProvider.actionMarkAsRead();
-
- Account account = notificationData.getAccount();
- ArrayList messageReferences = notificationData.getAllMessageReferences();
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- PendingIntent markAllAsReadPendingIntent =
- actionCreator.createMarkAllAsReadPendingIntent(account, messageReferences, notificationId);
-
- builder.addAction(icon, title, markAllAsReadPendingIntent);
- }
-
- private void addDeleteAllAction(Builder builder, NotificationData notificationData) {
- if (K9.getNotificationQuickDeleteBehaviour() != NotificationQuickDelete.ALWAYS) {
- return;
- }
-
- int icon = resourceProvider.getIconDelete();
- String title = resourceProvider.actionDelete();
-
- Account account = notificationData.getAccount();
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- ArrayList messageReferences = notificationData.getAllMessageReferences();
- PendingIntent action = actionCreator.createDeleteAllPendingIntent(account, messageReferences, notificationId);
-
- builder.addAction(icon, title, action);
- }
-
- private void addDeleteAction(Builder builder, NotificationContent content, int notificationId) {
- if (!isDeleteActionEnabled()) {
- return;
- }
-
- int icon = resourceProvider.getIconDelete();
- String title = resourceProvider.actionDelete();
-
- MessageReference messageReference = content.messageReference;
- PendingIntent action = actionCreator.createDeleteMessagePendingIntent(messageReference, notificationId);
-
- builder.addAction(icon, title, action);
- }
-
- private void addReplyAction(Builder builder, NotificationContent content, int notificationId) {
- int icon = resourceProvider.getIconReply();
- String title = resourceProvider.actionReply();
-
- MessageReference messageReference = content.messageReference;
- PendingIntent replyToMessagePendingIntent =
- actionCreator.createReplyPendingIntent(messageReference, notificationId);
-
- builder.addAction(icon, title, replyToMessagePendingIntent);
- }
-
- private boolean isPrivacyModeActive() {
- KeyguardManager keyguardService = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
-
- boolean privacyModeAlwaysEnabled = K9.getNotificationHideSubject() == NotificationHideSubject.ALWAYS;
- boolean privacyModeEnabledWhenLocked = K9.getNotificationHideSubject() == NotificationHideSubject.WHEN_LOCKED;
- boolean screenLocked = keyguardService.inKeyguardRestrictedInputMode();
-
- return privacyModeAlwaysEnabled || (privacyModeEnabledWhenLocked && screenLocked);
- }
-
- protected InboxStyle createInboxStyle(Builder builder) {
- return new InboxStyle(builder);
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/LockScreenNotification.java b/app/core/src/main/java/com/fsck/k9/notification/LockScreenNotification.java
deleted file mode 100644
index 3884f3276273f98269282ac6521363c1a00c7361..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/LockScreenNotification.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
-import android.app.Notification;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.Builder;
-import android.text.TextUtils;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-
-
-class LockScreenNotification {
- static final int MAX_NUMBER_OF_SENDERS_IN_LOCK_SCREEN_NOTIFICATION = 5;
-
-
- private final NotificationHelper notificationHelper;
- private final NotificationResourceProvider resourceProvider;
-
-
- LockScreenNotification(NotificationHelper notificationHelper, NotificationResourceProvider resourceProvider) {
- this.notificationHelper = notificationHelper;
- this.resourceProvider = resourceProvider;
- }
-
- public void configureLockScreenNotification(Builder builder, NotificationData notificationData) {
- switch (K9.getLockScreenNotificationVisibility()) {
- case NOTHING: {
- builder.setVisibility(NotificationCompat.VISIBILITY_SECRET);
- break;
- }
- case APP_NAME: {
- builder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
- break;
- }
- case EVERYTHING: {
- builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
- break;
- }
- case SENDERS: {
- Notification publicNotification = createPublicNotificationWithSenderList(notificationData);
- builder.setPublicVersion(publicNotification);
- break;
- }
- case MESSAGE_COUNT: {
- Notification publicNotification = createPublicNotificationWithNewMessagesCount(notificationData);
- builder.setPublicVersion(publicNotification);
- break;
- }
- }
- }
-
- private Notification createPublicNotificationWithSenderList(NotificationData notificationData) {
- Builder builder = createPublicNotification(notificationData);
- int newMessages = notificationData.getNewMessagesCount();
- if (newMessages == 1) {
- NotificationHolder holder = notificationData.getHolderForLatestNotification();
- builder.setContentText(holder.content.sender);
- } else {
- List contents = notificationData.getContentForSummaryNotification();
- String senderList = createCommaSeparatedListOfSenders(contents);
- builder.setContentText(senderList);
- }
-
- return builder.build();
- }
-
- private Notification createPublicNotificationWithNewMessagesCount(NotificationData notificationData) {
- Builder builder = createPublicNotification(notificationData);
- Account account = notificationData.getAccount();
- String accountName = notificationHelper.getAccountName(account);
- builder.setContentText(accountName);
-
- return builder.build();
- }
-
- private Builder createPublicNotification(NotificationData notificationData) {
- Account account = notificationData.getAccount();
- int newMessages = notificationData.getNewMessagesCount();
- int unreadCount = notificationData.getUnreadMessageCount();
- String title = resourceProvider.newMessagesTitle(newMessages);
-
- return notificationHelper.createNotificationBuilder(account,
- NotificationChannelManager.ChannelType.MESSAGES)
- .setSmallIcon(resourceProvider.getIconNewMail())
- .setColor(account.getChipColor())
- .setNumber(unreadCount)
- .setContentTitle(title)
- .setCategory(NotificationCompat.CATEGORY_EMAIL);
- }
-
-
- String createCommaSeparatedListOfSenders(List contents) {
- // Use a LinkedHashSet so that we preserve ordering (newest to oldest), but still remove duplicates
- Set senders = new LinkedHashSet<>(MAX_NUMBER_OF_SENDERS_IN_LOCK_SCREEN_NOTIFICATION);
- for (NotificationContent content : contents) {
- senders.add(content.sender);
- if (senders.size() == MAX_NUMBER_OF_SENDERS_IN_LOCK_SCREEN_NOTIFICATION) {
- break;
- }
- }
-
- return TextUtils.join(", ", senders);
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/LockScreenNotificationCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/LockScreenNotificationCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e071201c0dac8659ebff93614f9a332094354f85
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/LockScreenNotificationCreator.kt
@@ -0,0 +1,60 @@
+package com.fsck.k9.notification
+
+import android.app.Notification
+import androidx.core.app.NotificationCompat
+
+internal class LockScreenNotificationCreator(
+ private val notificationHelper: NotificationHelper,
+ private val resourceProvider: NotificationResourceProvider
+) {
+ fun configureLockScreenNotification(
+ builder: NotificationCompat.Builder,
+ baseNotificationData: BaseNotificationData
+ ) {
+ when (baseNotificationData.lockScreenNotificationData) {
+ LockScreenNotificationData.None -> {
+ builder.setVisibility(NotificationCompat.VISIBILITY_SECRET)
+ }
+ LockScreenNotificationData.AppName -> {
+ builder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
+ }
+ LockScreenNotificationData.Public -> {
+ builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ }
+ is LockScreenNotificationData.SenderNames -> {
+ val publicNotification = createPublicNotificationWithSenderList(baseNotificationData)
+ builder.setPublicVersion(publicNotification)
+ }
+ LockScreenNotificationData.MessageCount -> {
+ val publicNotification = createPublicNotificationWithNewMessagesCount(baseNotificationData)
+ builder.setPublicVersion(publicNotification)
+ }
+ }
+ }
+
+ private fun createPublicNotificationWithSenderList(baseNotificationData: BaseNotificationData): Notification {
+ val notificationData = baseNotificationData.lockScreenNotificationData as LockScreenNotificationData.SenderNames
+ return createPublicNotification(baseNotificationData)
+ .setContentText(notificationData.senderNames)
+ .build()
+ }
+
+ private fun createPublicNotificationWithNewMessagesCount(baseNotificationData: BaseNotificationData): Notification {
+ return createPublicNotification(baseNotificationData)
+ .setContentText(baseNotificationData.accountName)
+ .build()
+ }
+
+ private fun createPublicNotification(baseNotificationData: BaseNotificationData): NotificationCompat.Builder {
+ val account = baseNotificationData.account
+ val newMessagesCount = baseNotificationData.newMessagesCount
+ val title = resourceProvider.newMessagesTitle(newMessagesCount)
+
+ return notificationHelper.createNotificationBuilder(account, NotificationChannelManager.ChannelType.MESSAGES)
+ .setSmallIcon(resourceProvider.iconNewMail)
+ .setColor(baseNotificationData.color)
+ .setNumber(newMessagesCount)
+ .setContentTitle(title)
+ .setCategory(NotificationCompat.CATEGORY_EMAIL)
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..190144862113200f25529bceef710962cc23c44e
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationController.kt
@@ -0,0 +1,86 @@
+package com.fsck.k9.notification
+
+import androidx.core.app.NotificationManagerCompat
+import com.fsck.k9.Account
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.mailstore.LocalMessage
+
+/**
+ * Handle notifications for new messages.
+ */
+internal class NewMailNotificationController(
+ private val notificationManager: NotificationManagerCompat,
+ private val newMailNotificationManager: NewMailNotificationManager,
+ private val summaryNotificationCreator: SummaryNotificationCreator,
+ private val singleMessageNotificationCreator: SingleMessageNotificationCreator
+) {
+ fun restoreNewMailNotifications(accounts: List) {
+ for (account in accounts) {
+ val notificationData = newMailNotificationManager.restoreNewMailNotifications(account)
+
+ if (notificationData != null) {
+ processNewMailNotificationData(notificationData)
+ }
+ }
+ }
+
+ fun addNewMailNotification(account: Account, message: LocalMessage, silent: Boolean) {
+ val notificationData = newMailNotificationManager.addNewMailNotification(account, message, silent)
+
+ processNewMailNotificationData(notificationData)
+ }
+
+ fun removeNewMailNotifications(
+ account: Account,
+ clearNewMessageState: Boolean,
+ selector: (List) -> List
+ ) {
+ val notificationData = newMailNotificationManager.removeNewMailNotifications(
+ account,
+ clearNewMessageState,
+ selector
+ )
+
+ if (notificationData != null) {
+ processNewMailNotificationData(notificationData)
+ }
+ }
+
+ fun clearNewMailNotifications(account: Account, clearNewMessageState: Boolean) {
+ val cancelNotificationIds = newMailNotificationManager.clearNewMailNotifications(account, clearNewMessageState)
+
+ cancelNotifications(cancelNotificationIds)
+ }
+
+ private fun processNewMailNotificationData(notificationData: NewMailNotificationData) {
+ cancelNotifications(notificationData.cancelNotificationIds)
+
+ for (singleNotificationData in notificationData.singleNotificationData) {
+ createSingleNotification(notificationData.baseNotificationData, singleNotificationData)
+ }
+
+ notificationData.summaryNotificationData?.let { summaryNotificationData ->
+ createSummaryNotification(notificationData.baseNotificationData, summaryNotificationData)
+ }
+ }
+
+ private fun cancelNotifications(notificationIds: List) {
+ for (notificationId in notificationIds) {
+ notificationManager.cancel(notificationId)
+ }
+ }
+
+ private fun createSingleNotification(
+ baseNotificationData: BaseNotificationData,
+ singleNotificationData: SingleNotificationData
+ ) {
+ singleMessageNotificationCreator.createSingleNotification(baseNotificationData, singleNotificationData)
+ }
+
+ private fun createSummaryNotification(
+ baseNotificationData: BaseNotificationData,
+ summaryNotificationData: SummaryNotificationData
+ ) {
+ summaryNotificationCreator.createSummaryNotification(baseNotificationData, summaryNotificationData)
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationData.kt b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationData.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9069b4f85ada8b1d1e66f855afff48b7df5df598
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationData.kt
@@ -0,0 +1,87 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.controller.MessageReference
+
+internal data class NewMailNotificationData(
+ val cancelNotificationIds: List,
+ val baseNotificationData: BaseNotificationData,
+ val singleNotificationData: List,
+ val summaryNotificationData: SummaryNotificationData?
+)
+
+internal data class BaseNotificationData(
+ val account: Account,
+ val accountName: String,
+ val groupKey: String,
+ val color: Int,
+ val newMessagesCount: Int,
+ val lockScreenNotificationData: LockScreenNotificationData,
+ val appearance: NotificationAppearance
+)
+
+internal sealed interface LockScreenNotificationData {
+ object None : LockScreenNotificationData
+ object AppName : LockScreenNotificationData
+ object Public : LockScreenNotificationData
+ object MessageCount : LockScreenNotificationData
+ data class SenderNames(val senderNames: String) : LockScreenNotificationData
+}
+
+internal data class NotificationAppearance(
+ val ringtone: String?,
+ val vibrationPattern: LongArray?,
+ val ledColor: Int?
+)
+
+internal data class SingleNotificationData(
+ val notificationId: Int,
+ val isSilent: Boolean,
+ val timestamp: Long,
+ val content: NotificationContent,
+ val actions: List,
+ val wearActions: List,
+ val addLockScreenNotification: Boolean
+)
+
+internal sealed interface SummaryNotificationData
+
+internal data class SummarySingleNotificationData(
+ val singleNotificationData: SingleNotificationData
+) : SummaryNotificationData
+
+internal data class SummaryInboxNotificationData(
+ val notificationId: Int,
+ val isSilent: Boolean,
+ val timestamp: Long,
+ val content: List,
+ val additionalMessagesCount: Int,
+ val messageReferences: List,
+ val actions: List,
+ val wearActions: List
+) : SummaryNotificationData
+
+internal enum class NotificationAction {
+ Reply,
+ MarkAsRead,
+ Delete
+}
+
+internal enum class WearNotificationAction {
+ Reply,
+ MarkAsRead,
+ Delete,
+ Archive,
+ Spam
+}
+
+internal enum class SummaryNotificationAction {
+ MarkAsRead,
+ Delete
+}
+
+internal enum class SummaryWearNotificationAction {
+ MarkAsRead,
+ Delete,
+ Archive
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..8e1a8c3fe2645aacaaf5b48b7d083b458f862ffa
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationManager.kt
@@ -0,0 +1,134 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.Clock
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.mailstore.LocalMessage
+
+/**
+ * Manages notifications for new messages
+ */
+internal class NewMailNotificationManager(
+ private val contentCreator: NotificationContentCreator,
+ private val notificationRepository: NotificationRepository,
+ private val baseNotificationDataCreator: BaseNotificationDataCreator,
+ private val singleMessageNotificationDataCreator: SingleMessageNotificationDataCreator,
+ private val summaryNotificationDataCreator: SummaryNotificationDataCreator,
+ private val clock: Clock
+) {
+ fun restoreNewMailNotifications(account: Account): NewMailNotificationData? {
+ val notificationData = notificationRepository.restoreNotifications(account) ?: return null
+
+ val addLockScreenNotification = notificationData.isSingleMessageNotification
+ val singleNotificationDataList = notificationData.activeNotifications.map { notificationHolder ->
+ createSingleNotificationData(
+ account = account,
+ notificationId = notificationHolder.notificationId,
+ content = notificationHolder.content,
+ timestamp = notificationHolder.timestamp,
+ addLockScreenNotification = addLockScreenNotification
+ )
+ }
+
+ return NewMailNotificationData(
+ cancelNotificationIds = emptyList(),
+ baseNotificationData = createBaseNotificationData(notificationData),
+ singleNotificationData = singleNotificationDataList,
+ summaryNotificationData = createSummaryNotificationData(notificationData, silent = true)
+ )
+ }
+
+ fun addNewMailNotification(account: Account, message: LocalMessage, silent: Boolean): NewMailNotificationData {
+ val content = contentCreator.createFromMessage(account, message)
+
+ val result = notificationRepository.addNotification(account, content, timestamp = now())
+
+ val singleNotificationData = createSingleNotificationData(
+ account = account,
+ notificationId = result.notificationHolder.notificationId,
+ content = result.notificationHolder.content,
+ timestamp = result.notificationHolder.timestamp,
+ addLockScreenNotification = result.notificationData.isSingleMessageNotification
+ )
+
+ return NewMailNotificationData(
+ cancelNotificationIds = if (result.shouldCancelNotification) {
+ listOf(result.cancelNotificationId)
+ } else {
+ emptyList()
+ },
+ baseNotificationData = createBaseNotificationData(result.notificationData),
+ singleNotificationData = listOf(singleNotificationData),
+ summaryNotificationData = createSummaryNotificationData(result.notificationData, silent)
+ )
+ }
+
+ fun removeNewMailNotifications(
+ account: Account,
+ clearNewMessageState: Boolean,
+ selector: (List) -> List
+ ): NewMailNotificationData? {
+ val result = notificationRepository.removeNotifications(account, clearNewMessageState, selector) ?: return null
+
+ val cancelNotificationIds = when {
+ result.notificationData.isEmpty() -> {
+ result.cancelNotificationIds + NotificationIds.getNewMailSummaryNotificationId(account)
+ }
+ else -> {
+ result.cancelNotificationIds
+ }
+ }
+
+ val singleNotificationData = result.notificationHolders.map { notificationHolder ->
+ createSingleNotificationData(
+ account = account,
+ notificationId = notificationHolder.notificationId,
+ content = notificationHolder.content,
+ timestamp = notificationHolder.timestamp,
+ addLockScreenNotification = result.notificationData.isSingleMessageNotification
+ )
+ }
+
+ return NewMailNotificationData(
+ cancelNotificationIds = cancelNotificationIds,
+ baseNotificationData = createBaseNotificationData(result.notificationData),
+ singleNotificationData = singleNotificationData,
+ summaryNotificationData = createSummaryNotificationData(result.notificationData, silent = true)
+ )
+ }
+
+ fun clearNewMailNotifications(account: Account, clearNewMessageState: Boolean): List {
+ notificationRepository.clearNotifications(account, clearNewMessageState)
+ return NotificationIds.getAllMessageNotificationIds(account)
+ }
+
+ private fun createBaseNotificationData(notificationData: NotificationData): BaseNotificationData {
+ return baseNotificationDataCreator.createBaseNotificationData(notificationData)
+ }
+
+ private fun createSingleNotificationData(
+ account: Account,
+ notificationId: Int,
+ content: NotificationContent,
+ timestamp: Long,
+ addLockScreenNotification: Boolean
+ ): SingleNotificationData {
+ return singleMessageNotificationDataCreator.createSingleNotificationData(
+ account,
+ notificationId,
+ content,
+ timestamp,
+ addLockScreenNotification
+ )
+ }
+
+ private fun createSummaryNotificationData(data: NotificationData, silent: Boolean): SummaryNotificationData? {
+ return if (data.isEmpty()) {
+ null
+ } else {
+ summaryNotificationDataCreator.createSummaryNotificationData(data, silent)
+ }
+ }
+
+ private fun now(): Long = clock.time
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NewMailNotifications.java b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotifications.java
deleted file mode 100644
index 8fc8ae5e270abcbce8e4f9911891b6c68a4e50d8..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/NewMailNotifications.java
+++ /dev/null
@@ -1,167 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.Notification;
-import androidx.core.app.NotificationManagerCompat;
-import android.util.SparseArray;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.K9.NotificationHideSubject;
-import com.fsck.k9.controller.MessageReference;
-import com.fsck.k9.mailstore.LocalMessage;
-
-
-/**
- * Handle notifications for new messages.
- *
- * We call the notification shown on the device summary notification, even when there's only one new message.
- * Notifications on an Android Wear device are displayed as a stack of cards and that's why we call them stacked
- * notifications. We have to keep track of stacked notifications individually and recreate/update the summary
- * notification when one or more of the stacked notifications are added/removed.
- * {@link NotificationData} keeps track of all data required to (re)create the actual system notifications.
- *
- */
-class NewMailNotifications {
- private final NotificationHelper notificationHelper;
- private final NotificationContentCreator contentCreator;
- private final DeviceNotifications deviceNotifications;
- private final WearNotifications wearNotifications;
- private final SparseArray notifications = new SparseArray<>();
- private final Object lock = new Object();
-
-
- NewMailNotifications(NotificationHelper notificationHelper, NotificationContentCreator contentCreator,
- DeviceNotifications deviceNotifications, WearNotifications wearNotifications) {
- this.notificationHelper = notificationHelper;
- this.deviceNotifications = deviceNotifications;
- this.wearNotifications = wearNotifications;
- this.contentCreator = contentCreator;
- }
-
- public void addNewMailNotification(Account account, LocalMessage message, int unreadMessageCount) {
- NotificationContent content = contentCreator.createFromMessage(account, message);
-
- synchronized (lock) {
- NotificationData notificationData = getOrCreateNotificationData(account, unreadMessageCount);
- AddNotificationResult result = notificationData.addNotificationContent(content);
-
- if (result.shouldCancelNotification()) {
- int notificationId = result.getNotificationId();
- cancelNotification(notificationId);
- }
-
- createStackedNotification(account, result.getNotificationHolder());
- createSummaryNotification(account, notificationData, false);
- }
- }
-
- public void removeNewMailNotification(Account account, MessageReference messageReference) {
- synchronized (lock) {
- NotificationData notificationData = getNotificationData(account);
- if (notificationData == null) {
- return;
- }
-
- RemoveNotificationResult result = notificationData.removeNotificationForMessage(messageReference);
- if (result.isUnknownNotification()) {
- return;
- }
-
- cancelNotification(result.getNotificationId());
-
- if (result.shouldCreateNotification()) {
- createStackedNotification(account, result.getNotificationHolder());
- }
-
- updateSummaryNotification(account, notificationData);
- }
- }
-
- public void clearNewMailNotifications(Account account) {
- NotificationData notificationData;
- synchronized (lock) {
- notificationData = removeNotificationData(account);
- }
-
- if (notificationData == null) {
- return;
- }
-
- for (int notificationId : notificationData.getActiveNotificationIds()) {
- cancelNotification(notificationId);
- }
-
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- cancelNotification(notificationId);
- }
-
- private NotificationData getOrCreateNotificationData(Account account, int unreadMessageCount) {
- NotificationData notificationData = getNotificationData(account);
- if (notificationData != null) {
- return notificationData;
- }
-
- int accountNumber = account.getAccountNumber();
- NotificationData newNotificationHolder = createNotificationData(account, unreadMessageCount);
- notifications.put(accountNumber, newNotificationHolder);
-
- return newNotificationHolder;
- }
-
- private NotificationData getNotificationData(Account account) {
- int accountNumber = account.getAccountNumber();
- return notifications.get(accountNumber);
- }
-
- private NotificationData removeNotificationData(Account account) {
- int accountNumber = account.getAccountNumber();
- NotificationData notificationData = notifications.get(accountNumber);
- notifications.remove(accountNumber);
- return notificationData;
- }
-
- NotificationData createNotificationData(Account account, int unreadMessageCount) {
- NotificationData notificationData = new NotificationData(account);
- notificationData.setUnreadMessageCount(unreadMessageCount);
- return notificationData;
- }
-
- private void cancelNotification(int notificationId) {
- getNotificationManager().cancel(notificationId);
- }
-
- private void updateSummaryNotification(Account account, NotificationData notificationData) {
- if (notificationData.getNewMessagesCount() == 0) {
- clearNewMailNotifications(account);
- } else {
- createSummaryNotification(account, notificationData, true);
- }
- }
-
- private void createSummaryNotification(Account account, NotificationData notificationData, boolean silent) {
- Notification notification = deviceNotifications.buildSummaryNotification(account, notificationData, silent);
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
-
- getNotificationManager().notify(notificationId, notification);
- }
-
- private void createStackedNotification(Account account, NotificationHolder holder) {
- if (isPrivacyModeEnabled()) {
- return;
- }
-
- Notification notification = wearNotifications.buildStackedNotification(account, holder);
- int notificationId = holder.notificationId;
-
- getNotificationManager().notify(notificationId, notification);
- }
-
- private boolean isPrivacyModeEnabled() {
- return K9.getNotificationHideSubject() != NotificationHideSubject.NEVER;
- }
-
- private NotificationManagerCompat getNotificationManager() {
- return notificationHelper.getNotificationManager();
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.java b/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.java
deleted file mode 100644
index 94e781c4d8f835841d339d576ea274e3a8625c63..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.List;
-
-import android.app.PendingIntent;
-import android.content.Context;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.controller.MessageReference;
-
-
-public interface NotificationActionCreator {
- PendingIntent createViewMessagePendingIntent(MessageReference messageReference, int notificationId);
-
- PendingIntent createViewFolderPendingIntent(Account account, long folderId, int notificationId);
-
- PendingIntent createViewMessagesPendingIntent(Account account, List messageReferences,
- int notificationId);
-
- PendingIntent createViewFolderListPendingIntent(Account account, int notificationId);
-
- PendingIntent createDismissAllMessagesPendingIntent(Account account, int notificationId);
-
- PendingIntent createDismissMessagePendingIntent(Context context, MessageReference messageReference,
- int notificationId);
-
- PendingIntent createReplyPendingIntent(MessageReference messageReference, int notificationId);
-
- PendingIntent createMarkMessageAsReadPendingIntent(MessageReference messageReference, int notificationId);
-
- PendingIntent createMarkAllAsReadPendingIntent(Account account, List messageReferences,
- int notificationId);
-
- PendingIntent getEditIncomingServerSettingsIntent(Account account);
-
- PendingIntent getEditOutgoingServerSettingsIntent(Account account);
-
- PendingIntent createDeleteMessagePendingIntent(MessageReference messageReference, int notificationId);
-
- PendingIntent createDeleteAllPendingIntent(Account account, List messageReferences,
- int notificationId);
-
- PendingIntent createArchiveMessagePendingIntent(MessageReference messageReference, int notificationId);
-
- PendingIntent createArchiveAllPendingIntent(Account account, List messageReferences,
- int notificationId);
-
- PendingIntent createMarkMessageAsSpamPendingIntent(MessageReference messageReference, int notificationId);
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d7e403b8c315bbd1d65326ab97bddfd161bbc6d0
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.kt
@@ -0,0 +1,58 @@
+package com.fsck.k9.notification
+
+import android.app.PendingIntent
+import com.fsck.k9.Account
+import com.fsck.k9.controller.MessageReference
+
+interface NotificationActionCreator {
+ fun createViewMessagePendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent
+
+ fun createViewFolderPendingIntent(account: Account, folderId: Long, notificationId: Int): PendingIntent
+
+ fun createViewMessagesPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent
+
+ fun createViewFolderListPendingIntent(account: Account, notificationId: Int): PendingIntent
+
+ fun createDismissAllMessagesPendingIntent(account: Account, notificationId: Int): PendingIntent
+
+ fun createDismissMessagePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent
+
+ fun createReplyPendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent
+
+ fun createMarkMessageAsReadPendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent
+
+ fun createMarkAllAsReadPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent
+
+ fun getEditIncomingServerSettingsIntent(account: Account): PendingIntent
+
+ fun getEditOutgoingServerSettingsIntent(account: Account): PendingIntent
+
+ fun createDeleteMessagePendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent
+
+ fun createDeleteAllPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent
+
+ fun createArchiveMessagePendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent
+
+ fun createArchiveAllPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent
+
+ fun createMarkMessageAsSpamPendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationActionService.java b/app/core/src/main/java/com/fsck/k9/notification/NotificationActionService.java
deleted file mode 100644
index a0453579e60ed0420fed713c77eed9ece3546e9e..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationActionService.java
+++ /dev/null
@@ -1,247 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.ArrayList;
-import java.util.List;
-
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.IBinder;
-
-import androidx.annotation.Nullable;
-
-import timber.log.Timber;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.Preferences;
-import com.fsck.k9.controller.MessageReference;
-import com.fsck.k9.controller.MessagingController;
-import com.fsck.k9.mail.Flag;
-
-import static com.fsck.k9.controller.MessageReferenceHelper.toMessageReferenceList;
-import static com.fsck.k9.controller.MessageReferenceHelper.toMessageReferenceStringList;
-
-
-public class NotificationActionService extends Service {
- private static final String ACTION_MARK_AS_READ = "ACTION_MARK_AS_READ";
- private static final String ACTION_DELETE = "ACTION_DELETE";
- private static final String ACTION_ARCHIVE = "ACTION_ARCHIVE";
- private static final String ACTION_SPAM = "ACTION_SPAM";
- private static final String ACTION_DISMISS = "ACTION_DISMISS";
-
- private static final String EXTRA_ACCOUNT_UUID = "accountUuid";
- private static final String EXTRA_MESSAGE_REFERENCE = "messageReference";
- private static final String EXTRA_MESSAGE_REFERENCES = "messageReferences";
-
-
- public static Intent createMarkMessageAsReadIntent(Context context, MessageReference messageReference) {
- Intent intent = new Intent(context, NotificationActionService.class);
- intent.setAction(ACTION_MARK_AS_READ);
- intent.putExtra(EXTRA_ACCOUNT_UUID, messageReference.getAccountUuid());
- intent.putExtra(EXTRA_MESSAGE_REFERENCES, createSingleItemArrayList(messageReference));
-
- return intent;
- }
-
- public static Intent createMarkAllAsReadIntent(Context context, String accountUuid,
- List messageReferences) {
- Intent intent = new Intent(context, NotificationActionService.class);
- intent.setAction(ACTION_MARK_AS_READ);
- intent.putExtra(EXTRA_ACCOUNT_UUID, accountUuid);
- intent.putExtra(EXTRA_MESSAGE_REFERENCES, toMessageReferenceStringList(messageReferences));
-
- return intent;
- }
-
- public static Intent createDismissMessageIntent(Context context, MessageReference messageReference) {
- Intent intent = new Intent(context, NotificationActionService.class);
- intent.setAction(ACTION_DISMISS);
- intent.putExtra(EXTRA_ACCOUNT_UUID, messageReference.getAccountUuid());
- intent.putExtra(EXTRA_MESSAGE_REFERENCE, messageReference.toIdentityString());
-
- return intent;
- }
-
- public static Intent createDismissAllMessagesIntent(Context context, Account account) {
- Intent intent = new Intent(context, NotificationActionService.class);
- intent.setAction(ACTION_DISMISS);
- intent.putExtra(EXTRA_ACCOUNT_UUID, account.getUuid());
-
- return intent;
- }
-
- public static Intent createDeleteMessageIntent(Context context, MessageReference messageReference) {
- Intent intent = new Intent(context, NotificationActionService.class);
- intent.setAction(ACTION_DELETE);
- intent.putExtra(EXTRA_ACCOUNT_UUID, messageReference.getAccountUuid());
- intent.putExtra(EXTRA_MESSAGE_REFERENCES, createSingleItemArrayList(messageReference));
-
- return intent;
- }
-
- public static Intent createDeleteAllMessagesIntent(Context context, String accountUuid,
- List messageReferences) {
- Intent intent = new Intent(context, NotificationActionService.class);
- intent.setAction(ACTION_DELETE);
- intent.putExtra(EXTRA_ACCOUNT_UUID, accountUuid);
- intent.putExtra(EXTRA_MESSAGE_REFERENCES, toMessageReferenceStringList(messageReferences));
-
- return intent;
- }
-
- public static Intent createArchiveMessageIntent(Context context, MessageReference messageReference) {
- Intent intent = new Intent(context, NotificationActionService.class);
- intent.setAction(ACTION_ARCHIVE);
- intent.putExtra(EXTRA_ACCOUNT_UUID, messageReference.getAccountUuid());
- intent.putExtra(EXTRA_MESSAGE_REFERENCES, createSingleItemArrayList(messageReference));
-
- return intent;
- }
-
- public static Intent createArchiveAllIntent(Context context, Account account,
- List messageReferences) {
- Intent intent = new Intent(context, NotificationActionService.class);
- intent.setAction(ACTION_ARCHIVE);
- intent.putExtra(EXTRA_ACCOUNT_UUID, account.getUuid());
- intent.putExtra(EXTRA_MESSAGE_REFERENCES, toMessageReferenceStringList(messageReferences));
-
- return intent;
- }
-
- public static Intent createMarkMessageAsSpamIntent(Context context, MessageReference messageReference) {
- Intent intent = new Intent(context, NotificationActionService.class);
- intent.setAction(ACTION_SPAM);
- intent.putExtra(EXTRA_ACCOUNT_UUID, messageReference.getAccountUuid());
- intent.putExtra(EXTRA_MESSAGE_REFERENCE, messageReference.toIdentityString());
-
- return intent;
- }
-
- private static ArrayList createSingleItemArrayList(MessageReference messageReference) {
- ArrayList messageReferenceStrings = new ArrayList<>(1);
- messageReferenceStrings.add(messageReference.toIdentityString());
- return messageReferenceStrings;
- }
-
- @Override
- public final int onStartCommand(Intent intent, int flags, int startId) {
- Timber.i("NotificationActionService started with startId = %d", startId);
-
- String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT_UUID);
- Preferences preferences = Preferences.getPreferences(this);
- Account account = preferences.getAccount(accountUuid);
-
- if (account == null) {
- Timber.w("Could not find account for notification action.");
- return START_NOT_STICKY;
- }
-
- MessagingController controller = MessagingController.getInstance(getApplication());
-
- String action = intent.getAction();
- if (ACTION_MARK_AS_READ.equals(action)) {
- markMessagesAsRead(intent, account, controller);
- } else if (ACTION_DELETE.equals(action)) {
- deleteMessages(intent, controller);
- } else if (ACTION_ARCHIVE.equals(action)) {
- archiveMessages(intent, account, controller);
- } else if (ACTION_SPAM.equals(action)) {
- markMessageAsSpam(intent, account, controller);
- } else if (ACTION_DISMISS.equals(action)) {
- Timber.i("Notification dismissed");
- }
-
- cancelNotifications(intent, account, controller);
-
- return START_NOT_STICKY;
- }
-
- @Nullable
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- private void markMessagesAsRead(Intent intent, Account account, MessagingController controller) {
- Timber.i("NotificationActionService marking messages as read");
-
- List messageReferenceStrings = intent.getStringArrayListExtra(EXTRA_MESSAGE_REFERENCES);
- List messageReferences = toMessageReferenceList(messageReferenceStrings);
- for (MessageReference messageReference : messageReferences) {
- long folderId = messageReference.getFolderId();
- String uid = messageReference.getUid();
- controller.setFlag(account, folderId, uid, Flag.SEEN, true);
- }
- }
-
- private void deleteMessages(Intent intent, MessagingController controller) {
- Timber.i("NotificationActionService deleting messages");
-
- List messageReferenceStrings = intent.getStringArrayListExtra(EXTRA_MESSAGE_REFERENCES);
- List messageReferences = toMessageReferenceList(messageReferenceStrings);
- controller.deleteMessages(messageReferences);
- }
-
- private void archiveMessages(Intent intent, Account account, MessagingController controller) {
- Timber.i("NotificationActionService archiving messages");
-
- Long archiveFolderId = account.getArchiveFolderId();
- if (!isMovePossible(controller, account, archiveFolderId)) {
- Timber.w("Can not archive messages");
- return;
- }
-
- List messageReferenceStrings = intent.getStringArrayListExtra(EXTRA_MESSAGE_REFERENCES);
- List messageReferences = toMessageReferenceList(messageReferenceStrings);
- for (MessageReference messageReference : messageReferences) {
- if (controller.isMoveCapable(messageReference)) {
- long sourceFolderId = messageReference.getFolderId();
- controller.moveMessage(account, sourceFolderId, messageReference, archiveFolderId);
- }
- }
- }
-
- private void markMessageAsSpam(Intent intent, Account account, MessagingController controller) {
- Timber.i("NotificationActionService moving messages to spam");
-
- String messageReferenceString = intent.getStringExtra(EXTRA_MESSAGE_REFERENCE);
- MessageReference messageReference = MessageReference.parse(messageReferenceString);
- if (messageReference == null) {
- Timber.w("Invalid message reference: %s", messageReferenceString);
- return;
- }
-
- Long spamFolderId = account.getSpamFolderId();
- if (!K9.isConfirmSpam() && isMovePossible(controller, account, spamFolderId)) {
- long sourceFolderId = messageReference.getFolderId();
- controller.moveMessage(account, sourceFolderId, messageReference, spamFolderId);
- }
- }
-
- private void cancelNotifications(Intent intent, Account account, MessagingController controller) {
- if (intent.hasExtra(EXTRA_MESSAGE_REFERENCE)) {
- String messageReferenceString = intent.getStringExtra(EXTRA_MESSAGE_REFERENCE);
- MessageReference messageReference = MessageReference.parse(messageReferenceString);
- if (messageReference != null) {
- controller.cancelNotificationForMessage(account, messageReference);
- } else {
- Timber.w("Invalid message reference: %s", messageReferenceString);
- }
- } else if (intent.hasExtra(EXTRA_MESSAGE_REFERENCES)) {
- List messageReferenceStrings = intent.getStringArrayListExtra(EXTRA_MESSAGE_REFERENCES);
- List messageReferences = toMessageReferenceList(messageReferenceStrings);
- for (MessageReference messageReference : messageReferences) {
- controller.cancelNotificationForMessage(account, messageReference);
- }
- } else {
- controller.cancelNotificationsForAccount(account);
- }
- }
-
- private boolean isMovePossible(MessagingController controller, Account account, Long destinationFolderId) {
- boolean isSpecialFolderConfigured = destinationFolderId != null;
- return isSpecialFolderConfigured && controller.isMoveCapable(account);
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationActionService.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationActionService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d70ea87778326e3c891c99868bf21dc387dd459c
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationActionService.kt
@@ -0,0 +1,244 @@
+package com.fsck.k9.notification
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.IBinder
+import com.fsck.k9.Account
+import com.fsck.k9.K9
+import com.fsck.k9.Preferences
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.controller.MessageReferenceHelper
+import com.fsck.k9.controller.MessagingController
+import com.fsck.k9.mail.Flag
+import org.koin.android.ext.android.inject
+import timber.log.Timber
+
+class NotificationActionService : Service() {
+ private val preferences: Preferences by inject()
+ private val messagingController: MessagingController by inject()
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ Timber.i("NotificationActionService started with startId = %d", startId)
+
+ val accountUuid = intent.getStringExtra(EXTRA_ACCOUNT_UUID) ?: error("Missing account UUID")
+
+ val account = preferences.getAccount(accountUuid)
+ if (account == null) {
+ Timber.w("Could not find account for notification action.")
+ return START_NOT_STICKY
+ }
+
+ when (intent.action) {
+ ACTION_MARK_AS_READ -> markMessagesAsRead(intent, account)
+ ACTION_DELETE -> deleteMessages(intent)
+ ACTION_ARCHIVE -> archiveMessages(intent, account)
+ ACTION_SPAM -> markMessageAsSpam(intent, account)
+ ACTION_DISMISS -> Timber.i("Notification dismissed")
+ }
+
+ cancelNotifications(intent, account)
+
+ return START_NOT_STICKY
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ private fun markMessagesAsRead(intent: Intent, account: Account) {
+ Timber.i("NotificationActionService marking messages as read")
+
+ val messageReferenceStrings = intent.getStringArrayListExtra(EXTRA_MESSAGE_REFERENCES)
+ val messageReferences = MessageReferenceHelper.toMessageReferenceList(messageReferenceStrings)
+
+ for (messageReference in messageReferences) {
+ val folderId = messageReference.folderId
+ val uid = messageReference.uid
+ messagingController.setFlag(account, folderId, uid, Flag.SEEN, true)
+ }
+ }
+
+ private fun deleteMessages(intent: Intent) {
+ Timber.i("NotificationActionService deleting messages")
+
+ val messageReferenceStrings = intent.getStringArrayListExtra(EXTRA_MESSAGE_REFERENCES)
+ val messageReferences = MessageReferenceHelper.toMessageReferenceList(messageReferenceStrings)
+
+ messagingController.deleteMessages(messageReferences)
+ }
+
+ private fun archiveMessages(intent: Intent, account: Account) {
+ Timber.i("NotificationActionService archiving messages")
+
+ val archiveFolderId = account.archiveFolderId
+ if (archiveFolderId == null || !messagingController.isMoveCapable(account)) {
+ Timber.w("Cannot archive messages")
+ return
+ }
+
+ val messageReferenceStrings = intent.getStringArrayListExtra(EXTRA_MESSAGE_REFERENCES)
+ val messageReferences = MessageReferenceHelper.toMessageReferenceList(messageReferenceStrings)
+
+ for (messageReference in messageReferences) {
+ if (messagingController.isMoveCapable(messageReference)) {
+ val sourceFolderId = messageReference.folderId
+ messagingController.moveMessage(account, sourceFolderId, messageReference, archiveFolderId)
+ }
+ }
+ }
+
+ private fun markMessageAsSpam(intent: Intent, account: Account) {
+ Timber.i("NotificationActionService moving messages to spam")
+
+ val messageReferenceString = intent.getStringExtra(EXTRA_MESSAGE_REFERENCE)
+ val messageReference = MessageReference.parse(messageReferenceString)
+
+ if (messageReference == null) {
+ Timber.w("Invalid message reference: %s", messageReferenceString)
+ return
+ }
+
+ val spamFolderId = account.spamFolderId
+ if (spamFolderId == null) {
+ Timber.w("No spam folder configured")
+ return
+ }
+
+ if (!K9.isConfirmSpam && messagingController.isMoveCapable(account)) {
+ val sourceFolderId = messageReference.folderId
+ messagingController.moveMessage(account, sourceFolderId, messageReference, spamFolderId)
+ }
+ }
+
+ private fun cancelNotifications(intent: Intent, account: Account) {
+ if (intent.hasExtra(EXTRA_MESSAGE_REFERENCE)) {
+ val messageReferenceString = intent.getStringExtra(EXTRA_MESSAGE_REFERENCE)
+ val messageReference = MessageReference.parse(messageReferenceString)
+
+ if (messageReference != null) {
+ messagingController.cancelNotificationForMessage(account, messageReference)
+ } else {
+ Timber.w("Invalid message reference: %s", messageReferenceString)
+ }
+ } else if (intent.hasExtra(EXTRA_MESSAGE_REFERENCES)) {
+ val messageReferenceStrings = intent.getStringArrayListExtra(EXTRA_MESSAGE_REFERENCES)
+ val messageReferences = MessageReferenceHelper.toMessageReferenceList(messageReferenceStrings)
+
+ for (messageReference in messageReferences) {
+ messagingController.cancelNotificationForMessage(account, messageReference)
+ }
+ } else {
+ messagingController.cancelNotificationsForAccount(account)
+ }
+ }
+
+ companion object {
+ private const val ACTION_MARK_AS_READ = "ACTION_MARK_AS_READ"
+ private const val ACTION_DELETE = "ACTION_DELETE"
+ private const val ACTION_ARCHIVE = "ACTION_ARCHIVE"
+ private const val ACTION_SPAM = "ACTION_SPAM"
+ private const val ACTION_DISMISS = "ACTION_DISMISS"
+ private const val EXTRA_ACCOUNT_UUID = "accountUuid"
+ private const val EXTRA_MESSAGE_REFERENCE = "messageReference"
+ private const val EXTRA_MESSAGE_REFERENCES = "messageReferences"
+
+ fun createMarkMessageAsReadIntent(context: Context, messageReference: MessageReference): Intent {
+ return Intent(context, NotificationActionService::class.java).apply {
+ action = ACTION_MARK_AS_READ
+ putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
+ putExtra(EXTRA_MESSAGE_REFERENCES, createSingleItemArrayList(messageReference))
+ }
+ }
+
+ fun createMarkAllAsReadIntent(
+ context: Context,
+ accountUuid: String,
+ messageReferences: List
+ ): Intent {
+ return Intent(context, NotificationActionService::class.java).apply {
+ action = ACTION_MARK_AS_READ
+ putExtra(EXTRA_ACCOUNT_UUID, accountUuid)
+ putExtra(
+ EXTRA_MESSAGE_REFERENCES,
+ MessageReferenceHelper.toMessageReferenceStringList(messageReferences)
+ )
+ }
+ }
+
+ fun createDismissMessageIntent(context: Context, messageReference: MessageReference): Intent {
+ return Intent(context, NotificationActionService::class.java).apply {
+ action = ACTION_DISMISS
+ putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
+ putExtra(EXTRA_MESSAGE_REFERENCE, messageReference.toIdentityString())
+ }
+ }
+
+ fun createDismissAllMessagesIntent(context: Context, account: Account): Intent {
+ return Intent(context, NotificationActionService::class.java).apply {
+ action = ACTION_DISMISS
+ putExtra(EXTRA_ACCOUNT_UUID, account.uuid)
+ }
+ }
+
+ fun createDeleteMessageIntent(context: Context, messageReference: MessageReference): Intent {
+ return Intent(context, NotificationActionService::class.java).apply {
+ action = ACTION_DELETE
+ putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
+ putExtra(EXTRA_MESSAGE_REFERENCES, createSingleItemArrayList(messageReference))
+ }
+ }
+
+ fun createDeleteAllMessagesIntent(
+ context: Context,
+ accountUuid: String,
+ messageReferences: List
+ ): Intent {
+ return Intent(context, NotificationActionService::class.java).apply {
+ action = ACTION_DELETE
+ putExtra(EXTRA_ACCOUNT_UUID, accountUuid)
+ putExtra(
+ EXTRA_MESSAGE_REFERENCES,
+ MessageReferenceHelper.toMessageReferenceStringList(messageReferences)
+ )
+ }
+ }
+
+ fun createArchiveMessageIntent(context: Context, messageReference: MessageReference): Intent {
+ return Intent(context, NotificationActionService::class.java).apply {
+ action = ACTION_ARCHIVE
+ putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
+ putExtra(EXTRA_MESSAGE_REFERENCES, createSingleItemArrayList(messageReference))
+ }
+ }
+
+ fun createArchiveAllIntent(
+ context: Context,
+ account: Account,
+ messageReferences: List
+ ): Intent {
+ return Intent(context, NotificationActionService::class.java).apply {
+ action = ACTION_ARCHIVE
+ putExtra(EXTRA_ACCOUNT_UUID, account.uuid)
+ putExtra(
+ EXTRA_MESSAGE_REFERENCES,
+ MessageReferenceHelper.toMessageReferenceStringList(messageReferences)
+ )
+ }
+ }
+
+ fun createMarkMessageAsSpamIntent(context: Context, messageReference: MessageReference): Intent {
+ return Intent(context, NotificationActionService::class.java).apply {
+ action = ACTION_SPAM
+ putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
+ putExtra(EXTRA_MESSAGE_REFERENCE, messageReference.toIdentityString())
+ }
+ }
+
+ private fun createSingleItemArrayList(messageReference: MessageReference): ArrayList {
+ return ArrayList(1).apply {
+ add(messageReference.toIdentityString())
+ }
+ }
+ }
+}
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 71c2e728e980ddc40ecc921fd26ad10aabeca6fb..9597b082d5cb3e3d387e6b6d5748fee11838ea5a 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
@@ -6,8 +6,10 @@ import android.app.NotificationManager
import android.os.Build
import androidx.annotation.RequiresApi
import com.fsck.k9.Account
+import com.fsck.k9.NotificationSetting
import com.fsck.k9.Preferences
import java.util.concurrent.Executor
+import timber.log.Timber
class NotificationChannelManager(
private val preferences: Preferences,
@@ -137,12 +139,91 @@ class NotificationChannelManager(
}
fun getChannelIdFor(account: Account, channelType: ChannelType): String {
- val accountUuid = account.uuid
-
return if (channelType == ChannelType.MESSAGES) {
- "messages_channel_$accountUuid"
+ "messages_channel_${account.uuid}${account.messagesNotificationChannelSuffix}"
} else {
- "miscellaneous_channel_$accountUuid"
+ "miscellaneous_channel_${account.uuid}"
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ fun getNotificationConfiguration(account: Account): NotificationConfiguration {
+ val channelId = getChannelIdFor(account, ChannelType.MESSAGES)
+ val notificationChannel = notificationManager.getNotificationChannel(channelId)
+
+ return NotificationConfiguration(
+ isBlinkLightsEnabled = notificationChannel.shouldShowLights(),
+ lightColor = notificationChannel.lightColor,
+ isVibrationEnabled = notificationChannel.shouldVibrate(),
+ vibrationPattern = notificationChannel.vibrationPattern?.toList()
+ )
+ }
+
+ fun recreateMessagesNotificationChannel(account: Account) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
+
+ val oldChannelId = getChannelIdFor(account, ChannelType.MESSAGES)
+ val oldNotificationChannel = notificationManager.getNotificationChannel(oldChannelId)
+
+ if (oldNotificationChannel.matches(account.notificationSetting)) {
+ Timber.v("Not recreating NotificationChannel. The current one already matches the app's settings.")
+ return
+ }
+
+ notificationManager.deleteNotificationChannel(oldChannelId)
+
+ account.incrementMessagesNotificationChannelVersion()
+
+ val newChannelId = getChannelIdFor(account, ChannelType.MESSAGES)
+ val channelName = resourceProvider.messagesChannelName
+ val importance = oldNotificationChannel.importance
+
+ val newNotificationChannel = NotificationChannel(newChannelId, channelName, importance).apply {
+ description = resourceProvider.messagesChannelDescription
+ group = account.uuid
+
+ copyPropertiesFrom(oldNotificationChannel)
+ copyPropertiesFrom(account.notificationSetting)
+ }
+
+ Timber.v("Recreating NotificationChannel(%s => %s)", oldChannelId, newChannelId)
+ notificationManager.createNotificationChannel(newNotificationChannel)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun NotificationChannel.matches(notificationSetting: NotificationSetting): Boolean {
+ return lightColor == notificationSetting.ledColor &&
+ vibrationPattern.contentEquals(notificationSetting.vibration)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun NotificationChannel.copyPropertiesFrom(otherNotificationChannel: NotificationChannel) {
+ setShowBadge(otherNotificationChannel.canShowBadge())
+ setSound(otherNotificationChannel.sound, otherNotificationChannel.audioAttributes)
+ enableVibration(otherNotificationChannel.shouldVibrate())
+ enableLights(otherNotificationChannel.shouldShowLights())
+ setBypassDnd(otherNotificationChannel.canBypassDnd())
+ lockscreenVisibility = otherNotificationChannel.lockscreenVisibility
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ setAllowBubbles(otherNotificationChannel.canBubble())
}
}
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun NotificationChannel.copyPropertiesFrom(notificationSetting: NotificationSetting) {
+ lightColor = notificationSetting.ledColor
+ if (shouldVibrate()) {
+ vibrationPattern = notificationSetting.vibration
+ }
+ }
+
+ private val Account.messagesNotificationChannelSuffix: String
+ get() = messagesNotificationChannelVersion.let { version -> if (version == 0) "" else "_$version" }
}
+
+data class NotificationConfiguration(
+ val isBlinkLightsEnabled: Boolean,
+ val lightColor: Int,
+ val isVibrationEnabled: Boolean,
+ val vibrationPattern: List?
+)
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationContent.java b/app/core/src/main/java/com/fsck/k9/notification/NotificationContent.java
deleted file mode 100644
index f3dc8cca4813e298ad9dfb3c740e3de214e96325..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationContent.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import com.fsck.k9.controller.MessageReference;
-
-
-class NotificationContent {
- public final MessageReference messageReference;
- public final String sender;
- public final String subject;
- public final CharSequence preview;
- public final CharSequence summary;
- public final boolean starred;
-
-
- public NotificationContent(MessageReference messageReference, String sender, String subject, CharSequence preview,
- CharSequence summary, boolean starred) {
- this.messageReference = messageReference;
- this.sender = sender;
- this.subject = subject;
- this.preview = preview;
- this.summary = summary;
- this.starred = starred;
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationContent.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationContent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..374979d2e101a7d45b7a878d7d0244b1646bf318
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationContent.kt
@@ -0,0 +1,11 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.controller.MessageReference
+
+internal data class NotificationContent(
+ val messageReference: MessageReference,
+ val sender: String,
+ val subject: String,
+ val preview: CharSequence,
+ val summary: CharSequence
+)
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationContentCreator.java b/app/core/src/main/java/com/fsck/k9/notification/NotificationContentCreator.java
deleted file mode 100644
index 6d98f508f3533c23831805fe400758ef7b37baeb..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationContentCreator.java
+++ /dev/null
@@ -1,129 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.content.Context;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.controller.MessageReference;
-import com.fsck.k9.helper.Contacts;
-import com.fsck.k9.helper.MessageHelper;
-import com.fsck.k9.mail.Address;
-import com.fsck.k9.mail.Flag;
-import com.fsck.k9.mail.Message;
-import com.fsck.k9.mailstore.LocalMessage;
-import com.fsck.k9.message.extractors.PreviewResult.PreviewType;
-
-
-class NotificationContentCreator {
- private final Context context;
- private final NotificationResourceProvider resourceProvider;
-
-
- public NotificationContentCreator(Context context, NotificationResourceProvider resourceProvider) {
- this.context = context;
- this.resourceProvider = resourceProvider;
- }
-
- public NotificationContent createFromMessage(Account account, LocalMessage message) {
- MessageReference messageReference = message.makeMessageReference();
- String sender = getMessageSender(account, message);
- String displaySender = getMessageSenderForDisplay(sender);
- String subject = getMessageSubject(message);
- CharSequence preview = getMessagePreview(message);
- CharSequence summary = buildMessageSummary(sender, subject);
- boolean starred = message.isSet(Flag.FLAGGED);
-
- return new NotificationContent(messageReference, displaySender, subject, preview, summary, starred);
- }
-
- private CharSequence getMessagePreview(LocalMessage message) {
- String subject = message.getSubject();
- String snippet = getPreview(message);
-
- boolean isSubjectEmpty = TextUtils.isEmpty(subject);
- boolean isSnippetPresent = snippet != null;
- if (isSubjectEmpty && isSnippetPresent) {
- return snippet;
- }
-
- String displaySubject = getMessageSubject(message);
-
- SpannableStringBuilder preview = new SpannableStringBuilder();
- preview.append(displaySubject);
- if (isSnippetPresent) {
- preview.append('\n');
- preview.append(snippet);
- }
-
- return preview;
- }
-
- private String getPreview(LocalMessage message) {
- PreviewType previewType = message.getPreviewType();
- switch (previewType) {
- case NONE:
- case ERROR:
- return null;
- case TEXT:
- return message.getPreview();
- case ENCRYPTED:
- return resourceProvider.previewEncrypted();
- }
-
- throw new AssertionError("Unknown preview type: " + previewType);
- }
-
- private CharSequence buildMessageSummary(String sender, String subject) {
- if (sender == null) {
- return subject;
- }
-
- SpannableStringBuilder summary = new SpannableStringBuilder();
- summary.append(sender);
- summary.append(" ");
- summary.append(subject);
-
- return summary;
- }
-
- private String getMessageSubject(Message message) {
- String subject = message.getSubject();
- if (!TextUtils.isEmpty(subject)) {
- return subject;
- }
-
- return resourceProvider.noSubject();
- }
-
- private String getMessageSender(Account account, Message message) {
- boolean isSelf = false;
- final Contacts contacts = K9.isShowContactName() ? Contacts.getInstance(context) : null;
- final Address[] fromAddresses = message.getFrom();
-
- if (fromAddresses != null) {
- isSelf = account.isAnIdentity(fromAddresses);
- if (!isSelf && fromAddresses.length > 0) {
- return MessageHelper.toFriendly(fromAddresses[0], contacts).toString();
- }
- }
-
- if (isSelf) {
- // show To: if the message was sent from me
- Address[] recipients = message.getRecipients(Message.RecipientType.TO);
-
- if (recipients != null && recipients.length > 0) {
- String recipientDisplayName = MessageHelper.toFriendly(recipients[0], contacts).toString();
- return resourceProvider.recipientDisplayName(recipientDisplayName);
- }
- }
-
- return null;
- }
-
- private String getMessageSenderForDisplay(String sender) {
- return (sender != null) ? sender : resourceProvider.noSender();
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationContentCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationContentCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..86d85b8efa8e3e81e06f079752228d61a40d2286
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationContentCreator.kt
@@ -0,0 +1,99 @@
+package com.fsck.k9.notification
+
+import android.content.Context
+import android.text.SpannableStringBuilder
+import com.fsck.k9.Account
+import com.fsck.k9.K9
+import com.fsck.k9.helper.Contacts
+import com.fsck.k9.helper.MessageHelper
+import com.fsck.k9.mail.Message
+import com.fsck.k9.mailstore.LocalMessage
+import com.fsck.k9.message.extractors.PreviewResult.PreviewType
+
+internal class NotificationContentCreator(
+ private val context: Context,
+ private val resourceProvider: NotificationResourceProvider
+) {
+ fun createFromMessage(account: Account, message: LocalMessage): NotificationContent {
+ val sender = getMessageSender(account, message)
+
+ return NotificationContent(
+ messageReference = message.makeMessageReference(),
+ sender = getMessageSenderForDisplay(sender),
+ subject = getMessageSubject(message),
+ preview = getMessagePreview(message),
+ summary = buildMessageSummary(sender, getMessageSubject(message))
+ )
+ }
+
+ private fun getMessagePreview(message: LocalMessage): CharSequence {
+ val snippet = getPreview(message)
+ if (message.subject.isNullOrEmpty() && snippet != null) {
+ return snippet
+ }
+
+ return SpannableStringBuilder().apply {
+ val displaySubject = getMessageSubject(message)
+ append(displaySubject)
+
+ if (snippet != null) {
+ append('\n')
+ append(snippet)
+ }
+ }
+ }
+
+ private fun getPreview(message: LocalMessage): String? {
+ val previewType = message.previewType ?: error("previewType == null")
+ return when (previewType) {
+ PreviewType.NONE, PreviewType.ERROR -> null
+ PreviewType.TEXT -> message.preview
+ PreviewType.ENCRYPTED -> resourceProvider.previewEncrypted()
+ }
+ }
+
+ private fun buildMessageSummary(sender: String?, subject: String): CharSequence {
+ return if (sender == null) {
+ subject
+ } else {
+ SpannableStringBuilder().apply {
+ append(sender)
+ append(" ")
+ append(subject)
+ }
+ }
+ }
+
+ private fun getMessageSubject(message: Message): String {
+ val subject = message.subject.orEmpty()
+ return subject.ifEmpty { resourceProvider.noSubject() }
+ }
+
+ private fun getMessageSender(account: Account, message: Message): String? {
+ val contacts = if (K9.isShowContactName) Contacts.getInstance(context) else null
+ var isSelf = false
+
+ val fromAddresses = message.from
+ if (!fromAddresses.isNullOrEmpty()) {
+ isSelf = account.isAnIdentity(fromAddresses)
+ if (!isSelf) {
+ return MessageHelper.toFriendly(fromAddresses.first(), contacts).toString()
+ }
+ }
+
+ if (isSelf) {
+ // show To: if the message was sent from me
+ val recipients = message.getRecipients(Message.RecipientType.TO)
+ if (!recipients.isNullOrEmpty()) {
+ val recipientDisplayName = MessageHelper.toFriendly(recipients.first(), contacts).toString()
+ return resourceProvider.recipientDisplayName(recipientDisplayName)
+ }
+ }
+
+ return null
+ }
+
+ private fun getMessageSenderForDisplay(sender: String?): String {
+ return sender ?: resourceProvider.noSender()
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationController.java b/app/core/src/main/java/com/fsck/k9/notification/NotificationController.java
deleted file mode 100644
index 1426f3e6f5953ee9ecba0d57ecc9a3fb8dc60da1..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationController.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import com.fsck.k9.Account;
-import com.fsck.k9.controller.MessageReference;
-import com.fsck.k9.mailstore.LocalFolder;
-import com.fsck.k9.mailstore.LocalMessage;
-
-
-public class NotificationController {
- private final CertificateErrorNotifications certificateErrorNotifications;
- private final AuthenticationErrorNotifications authenticationErrorNotifications;
- private final SyncNotifications syncNotifications;
- private final SendFailedNotifications sendFailedNotifications;
- private final NewMailNotifications newMailNotifications;
-
-
- NotificationController(
- CertificateErrorNotifications certificateErrorNotifications,
- AuthenticationErrorNotifications authenticationErrorNotifications,
- SyncNotifications syncNotifications,
- SendFailedNotifications sendFailedNotifications,
- NewMailNotifications newMailNotifications
- ) {
- this.certificateErrorNotifications = certificateErrorNotifications;
- this.authenticationErrorNotifications = authenticationErrorNotifications;
- this.syncNotifications = syncNotifications;
- this.sendFailedNotifications = sendFailedNotifications;
- this.newMailNotifications = newMailNotifications;
- }
-
- public void showCertificateErrorNotification(Account account, boolean incoming) {
- certificateErrorNotifications.showCertificateErrorNotification(account, incoming);
- }
-
- public void clearCertificateErrorNotifications(Account account, boolean incoming) {
- certificateErrorNotifications.clearCertificateErrorNotifications(account, incoming);
- }
-
- public void showAuthenticationErrorNotification(Account account, boolean incoming) {
- authenticationErrorNotifications.showAuthenticationErrorNotification(account, incoming);
- }
-
- public void clearAuthenticationErrorNotification(Account account, boolean incoming) {
- authenticationErrorNotifications.clearAuthenticationErrorNotification(account, incoming);
- }
-
- public void showSendingNotification(Account account) {
- syncNotifications.showSendingNotification(account);
- }
-
- public void clearSendingNotification(Account account) {
- syncNotifications.clearSendingNotification(account);
- }
-
- public void showSendFailedNotification(Account account, Exception exception) {
- sendFailedNotifications.showSendFailedNotification(account, exception);
- }
-
- public void clearSendFailedNotification(Account account) {
- sendFailedNotifications.clearSendFailedNotification(account);
- }
-
- public void showFetchingMailNotification(Account account, LocalFolder folder) {
- syncNotifications.showFetchingMailNotification(account, folder);
- }
-
- public void clearFetchingMailNotification(Account account) {
- syncNotifications.clearFetchingMailNotification(account);
- }
-
- public void addNewMailNotification(Account account, LocalMessage message, int previousUnreadMessageCount) {
- newMailNotifications.addNewMailNotification(account, message, previousUnreadMessageCount);
- }
-
- public void removeNewMailNotification(Account account, MessageReference messageReference) {
- newMailNotifications.removeNewMailNotification(account, messageReference);
- }
-
- public void clearNewMailNotifications(Account account) {
- newMailNotifications.clearNewMailNotifications(account);
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationController.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fe0144108e8d4c800d930123bb743c28811318d4
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationController.kt
@@ -0,0 +1,80 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.mailstore.LocalFolder
+import com.fsck.k9.mailstore.LocalMessage
+
+class NotificationController internal constructor(
+ private val certificateErrorNotificationController: CertificateErrorNotificationController,
+ private val authenticationErrorNotificationController: AuthenticationErrorNotificationController,
+ private val syncNotificationController: SyncNotificationController,
+ private val sendFailedNotificationController: SendFailedNotificationController,
+ private val newMailNotificationController: NewMailNotificationController
+) {
+ fun showCertificateErrorNotification(account: Account, incoming: Boolean) {
+ certificateErrorNotificationController.showCertificateErrorNotification(account, incoming)
+ }
+
+ fun clearCertificateErrorNotifications(account: Account, incoming: Boolean) {
+ certificateErrorNotificationController.clearCertificateErrorNotifications(account, incoming)
+ }
+
+ fun showAuthenticationErrorNotification(account: Account, incoming: Boolean) {
+ authenticationErrorNotificationController.showAuthenticationErrorNotification(account, incoming)
+ }
+
+ fun clearAuthenticationErrorNotification(account: Account, incoming: Boolean) {
+ authenticationErrorNotificationController.clearAuthenticationErrorNotification(account, incoming)
+ }
+
+ fun showSendingNotification(account: Account) {
+ syncNotificationController.showSendingNotification(account)
+ }
+
+ fun clearSendingNotification(account: Account) {
+ syncNotificationController.clearSendingNotification(account)
+ }
+
+ fun showSendFailedNotification(account: Account, exception: Exception) {
+ sendFailedNotificationController.showSendFailedNotification(account, exception)
+ }
+
+ fun clearSendFailedNotification(account: Account) {
+ sendFailedNotificationController.clearSendFailedNotification(account)
+ }
+
+ fun showFetchingMailNotification(account: Account, folder: LocalFolder) {
+ syncNotificationController.showFetchingMailNotification(account, folder)
+ }
+
+ fun showEmptyFetchingMailNotification(account: Account) {
+ syncNotificationController.showEmptyFetchingMailNotification(account)
+ }
+
+ fun clearFetchingMailNotification(account: Account) {
+ syncNotificationController.clearFetchingMailNotification(account)
+ }
+
+ fun restoreNewMailNotifications(accounts: List) {
+ newMailNotificationController.restoreNewMailNotifications(accounts)
+ }
+
+ fun addNewMailNotification(account: Account, message: LocalMessage, silent: Boolean) {
+ newMailNotificationController.addNewMailNotification(account, message, silent)
+ }
+
+ fun removeNewMailNotification(account: Account, messageReference: MessageReference) {
+ newMailNotificationController.removeNewMailNotifications(account, clearNewMessageState = true) {
+ listOf(messageReference)
+ }
+ }
+
+ fun clearNewMailNotifications(account: Account, selector: (List) -> List) {
+ newMailNotificationController.removeNewMailNotifications(account, clearNewMessageState = false, selector)
+ }
+
+ fun clearNewMailNotifications(account: Account, clearNewMessageState: Boolean) {
+ newMailNotificationController.clearNewMailNotifications(account, clearNewMessageState)
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationData.java b/app/core/src/main/java/com/fsck/k9/notification/NotificationData.java
deleted file mode 100644
index 69475eaa204306241f38cca2f45ec6465a691cc1..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationData.java
+++ /dev/null
@@ -1,224 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.ArrayList;
-import java.util.Deque;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-
-import android.util.SparseBooleanArray;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.controller.MessageReference;
-
-
-/**
- * A holder class for pending new mail notifications.
- */
-class NotificationData {
- // Note: As of Jellybean, phone notifications show a maximum of 5 lines, while tablet notifications show 7 lines.
- static final int MAX_NUMBER_OF_MESSAGES_FOR_SUMMARY_NOTIFICATION = 5;
- // Note: This class assumes MAX_NUMBER_OF_STACKED_NOTIFICATIONS >= MAX_NUMBER_OF_MESSAGES_FOR_SUMMARY_NOTIFICATION
- static final int MAX_NUMBER_OF_STACKED_NOTIFICATIONS = 8;
-
-
- private final Account account;
- private final LinkedList activeNotifications = new LinkedList<>();
- private final Deque additionalNotifications = new LinkedList<>();
- private final SparseBooleanArray notificationIdsInUse = new SparseBooleanArray();
- private int unreadMessageCount;
-
-
- public NotificationData(Account account) {
- this.account = account;
- }
-
- public AddNotificationResult addNotificationContent(NotificationContent content) {
- int notificationId;
- boolean cancelNotificationIdBeforeReuse;
- if (isMaxNumberOfActiveNotificationsReached()) {
- NotificationHolder notificationHolder = activeNotifications.removeLast();
- addToAdditionalNotifications(notificationHolder);
- notificationId = notificationHolder.notificationId;
- cancelNotificationIdBeforeReuse = true;
- } else {
- notificationId = getNewNotificationId();
- cancelNotificationIdBeforeReuse = false;
- }
-
- NotificationHolder notificationHolder = createNotificationHolder(notificationId, content);
- activeNotifications.addFirst(notificationHolder);
-
- if (cancelNotificationIdBeforeReuse) {
- return AddNotificationResult.replaceNotification(notificationHolder);
- } else {
- return AddNotificationResult.newNotification(notificationHolder);
- }
- }
-
- private boolean isMaxNumberOfActiveNotificationsReached() {
- return activeNotifications.size() == MAX_NUMBER_OF_STACKED_NOTIFICATIONS;
- }
-
- private void addToAdditionalNotifications(NotificationHolder notificationHolder) {
- additionalNotifications.addFirst(notificationHolder.content);
- }
-
- private int getNewNotificationId() {
- for (int i = 0; i < MAX_NUMBER_OF_STACKED_NOTIFICATIONS; i++) {
- int notificationId = NotificationIds.getNewMailStackedNotificationId(account, i);
- if (!isNotificationInUse(notificationId)) {
- markNotificationIdAsInUse(notificationId);
- return notificationId;
- }
- }
-
- throw new AssertionError("getNewNotificationId() called with no free notification ID");
- }
-
- private boolean isNotificationInUse(int notificationId) {
- return notificationIdsInUse.get(notificationId);
- }
-
- private void markNotificationIdAsInUse(int notificationId) {
- notificationIdsInUse.put(notificationId, true);
- }
-
- private void markNotificationIdAsFree(int notificationId) {
- notificationIdsInUse.delete(notificationId);
- }
-
- NotificationHolder createNotificationHolder(int notificationId, NotificationContent content) {
- return new NotificationHolder(notificationId, content);
- }
-
- public boolean containsStarredMessages() {
- for (NotificationHolder holder : activeNotifications) {
- if (holder.content.starred) {
- return true;
- }
- }
-
- for (NotificationContent content : additionalNotifications) {
- if (content.starred) {
- return true;
- }
- }
-
- return false;
- }
-
- public boolean hasSummaryOverflowMessages() {
- return activeNotifications.size() > MAX_NUMBER_OF_MESSAGES_FOR_SUMMARY_NOTIFICATION;
- }
-
- public int getSummaryOverflowMessagesCount() {
- int activeOverflowCount = activeNotifications.size() - MAX_NUMBER_OF_MESSAGES_FOR_SUMMARY_NOTIFICATION;
- if (activeOverflowCount > 0) {
- return activeOverflowCount + additionalNotifications.size();
- }
- return additionalNotifications.size();
- }
-
- public int getNewMessagesCount() {
- return activeNotifications.size() + additionalNotifications.size();
- }
-
- public boolean isSingleMessageNotification() {
- return activeNotifications.size() == 1;
- }
-
- public NotificationHolder getHolderForLatestNotification() {
- return activeNotifications.getFirst();
- }
-
- public List getContentForSummaryNotification() {
- int size = calculateNumberOfMessagesForSummaryNotification();
- List result = new ArrayList<>(size);
-
- Iterator iterator = activeNotifications.iterator();
- int notificationCount = 0;
- while (iterator.hasNext() && notificationCount < MAX_NUMBER_OF_MESSAGES_FOR_SUMMARY_NOTIFICATION) {
- NotificationHolder holder = iterator.next();
- result.add(holder.content);
- notificationCount++;
- }
-
- return result;
- }
-
- private int calculateNumberOfMessagesForSummaryNotification() {
- return Math.min(activeNotifications.size(), MAX_NUMBER_OF_MESSAGES_FOR_SUMMARY_NOTIFICATION);
- }
-
- public int[] getActiveNotificationIds() {
- int size = activeNotifications.size();
- int[] notificationIds = new int[size];
-
- for (int i = 0; i < size; i++) {
- NotificationHolder holder = activeNotifications.get(i);
- notificationIds[i] = holder.notificationId;
- }
-
- return notificationIds;
- }
-
- public RemoveNotificationResult removeNotificationForMessage(MessageReference messageReference) {
- NotificationHolder holder = getNotificationHolderForMessage(messageReference);
- if (holder == null) {
- return RemoveNotificationResult.unknownNotification();
- }
-
- activeNotifications.remove(holder);
-
- int notificationId = holder.notificationId;
- markNotificationIdAsFree(notificationId);
-
- if (!additionalNotifications.isEmpty()) {
- NotificationContent newContent = additionalNotifications.removeFirst();
- NotificationHolder replacement = createNotificationHolder(notificationId, newContent);
- activeNotifications.addLast(replacement);
- return RemoveNotificationResult.createNotification(replacement);
- }
-
- return RemoveNotificationResult.cancelNotification(notificationId);
- }
-
- private NotificationHolder getNotificationHolderForMessage(MessageReference messageReference) {
- for (NotificationHolder holder : activeNotifications) {
- if (messageReference.equals(holder.content.messageReference)) {
- return holder;
- }
- }
-
- return null;
- }
-
- public Account getAccount() {
- return account;
- }
-
- public int getUnreadMessageCount() {
- return unreadMessageCount + getNewMessagesCount();
- }
-
- public void setUnreadMessageCount(int unreadMessageCount) {
- this.unreadMessageCount = unreadMessageCount;
- }
-
- public ArrayList getAllMessageReferences() {
- int newSize = activeNotifications.size() + additionalNotifications.size();
- ArrayList messageReferences = new ArrayList<>(newSize);
-
- for (NotificationHolder holder : activeNotifications) {
- messageReferences.add(holder.content.messageReference);
- }
-
- for (NotificationContent content : additionalNotifications) {
- messageReferences.add(content.messageReference);
- }
-
- return messageReferences;
- }
-}
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
new file mode 100644
index 0000000000000000000000000000000000000000..c50985022b2d6637b5d28276eb9dc1e0d1823a02
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationData.kt
@@ -0,0 +1,40 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.controller.MessageReference
+
+/**
+ * Holds information about active and inactive new message notifications of an account.
+ */
+internal data class NotificationData(
+ val account: Account,
+ val activeNotifications: List,
+ val inactiveNotifications: List
+) {
+ val newMessagesCount: Int
+ get() = activeNotifications.size + inactiveNotifications.size
+
+ val isSingleMessageNotification: Boolean
+ get() = activeNotifications.size == 1
+
+ @OptIn(ExperimentalStdlibApi::class)
+ val messageReferences: List
+ get() {
+ return buildList(capacity = newMessagesCount) {
+ for (activeNotification in activeNotifications) {
+ add(activeNotification.content.messageReference)
+ }
+ for (inactiveNotification in inactiveNotifications) {
+ add(inactiveNotification.content.messageReference)
+ }
+ }
+ }
+
+ fun isEmpty() = activeNotifications.isEmpty()
+
+ companion object {
+ fun create(account: Account): NotificationData {
+ return NotificationData(account, activeNotifications = emptyList(), inactiveNotifications = emptyList())
+ }
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..51372b0a939d6c83d1815b49d19aff957ce0e443
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt
@@ -0,0 +1,177 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.controller.MessageReference
+
+internal const val MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS = 8
+
+/**
+ * Stores information about new message notifications for all accounts.
+ *
+ * We only use a limited number of system notifications per account (see [MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS]);
+ * those are called active notifications. The rest are called inactive notifications. When an active notification is
+ * removed, the latest inactive notification is promoted to an active notification.
+ */
+internal class NotificationDataStore {
+ private val notificationDataMap = mutableMapOf()
+
+ @Synchronized
+ fun initializeAccount(
+ account: Account,
+ activeNotifications: List,
+ inactiveNotifications: List
+ ): NotificationData {
+ require(activeNotifications.size <= MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS)
+
+ return NotificationData(account, activeNotifications, inactiveNotifications).also { notificationData ->
+ notificationDataMap[account.uuid] = notificationData
+ }
+ }
+
+ @Synchronized
+ fun addNotification(account: Account, content: NotificationContent, timestamp: Long): AddNotificationResult {
+ val notificationData = getNotificationData(account)
+
+ return if (notificationData.isMaxNumberOfActiveNotificationsReached) {
+ val lastNotificationHolder = notificationData.activeNotifications.last()
+ val inactiveNotificationHolder = lastNotificationHolder.toInactiveNotificationHolder()
+
+ val notificationId = lastNotificationHolder.notificationId
+ val notificationHolder = NotificationHolder(notificationId, timestamp, content)
+
+ val operations = listOf(
+ NotificationStoreOperation.ChangeToInactive(lastNotificationHolder.content.messageReference),
+ NotificationStoreOperation.Add(content.messageReference, notificationId, timestamp)
+ )
+
+ val newNotificationData = notificationData.copy(
+ activeNotifications = listOf(notificationHolder) + notificationData.activeNotifications.dropLast(1),
+ inactiveNotifications = listOf(inactiveNotificationHolder) + notificationData.inactiveNotifications
+ )
+ notificationDataMap[account.uuid] = newNotificationData
+
+ AddNotificationResult.replaceNotification(newNotificationData, operations, notificationHolder)
+ } else {
+ val notificationId = notificationData.getNewNotificationId()
+ val notificationHolder = NotificationHolder(notificationId, timestamp, content)
+
+ val operations = listOf(
+ NotificationStoreOperation.Add(content.messageReference, notificationId, timestamp)
+ )
+
+ val newNotificationData = notificationData.copy(
+ activeNotifications = listOf(notificationHolder) + notificationData.activeNotifications
+ )
+ notificationDataMap[account.uuid] = newNotificationData
+
+ AddNotificationResult.newNotification(newNotificationData, operations, notificationHolder)
+ }
+ }
+
+ @Synchronized
+ fun removeNotifications(
+ account: Account,
+ selector: (List) -> List
+ ): RemoveNotificationsResult? {
+ val notificationData = getNotificationData(account)
+ if (notificationData.isEmpty()) return null
+
+ val removeMessageReferences = selector.invoke(notificationData.messageReferences)
+
+ val operations = mutableListOf()
+ val newNotificationHolders = mutableListOf()
+ val cancelNotificationIds = mutableListOf()
+
+ for (messageReference in removeMessageReferences) {
+ val notificationHolder = notificationData.activeNotifications.firstOrNull {
+ it.content.messageReference == messageReference
+ }
+
+ if (notificationHolder == null) {
+ val inactiveNotificationHolder = notificationData.inactiveNotifications.firstOrNull {
+ it.content.messageReference == messageReference
+ } ?: continue
+
+ operations.add(NotificationStoreOperation.Remove(messageReference))
+
+ val newNotificationData = notificationData.copy(
+ inactiveNotifications = notificationData.inactiveNotifications - inactiveNotificationHolder
+ )
+ notificationDataMap[account.uuid] = newNotificationData
+ } else if (notificationData.inactiveNotifications.isNotEmpty()) {
+ val newNotificationHolder = notificationData.inactiveNotifications.first()
+ .toNotificationHolder(notificationHolder.notificationId)
+
+ newNotificationHolders.add(newNotificationHolder)
+ cancelNotificationIds.add(notificationHolder.notificationId)
+
+ operations.add(NotificationStoreOperation.Remove(messageReference))
+ operations.add(
+ NotificationStoreOperation.ChangeToActive(
+ newNotificationHolder.content.messageReference,
+ newNotificationHolder.notificationId
+ )
+ )
+
+ val newNotificationData = notificationData.copy(
+ activeNotifications = notificationData.activeNotifications - notificationHolder +
+ newNotificationHolder,
+ inactiveNotifications = notificationData.inactiveNotifications.drop(1)
+ )
+ notificationDataMap[account.uuid] = newNotificationData
+ } else {
+ cancelNotificationIds.add(notificationHolder.notificationId)
+
+ operations.add(NotificationStoreOperation.Remove(messageReference))
+
+ val newNotificationData = notificationData.copy(
+ activeNotifications = notificationData.activeNotifications - notificationHolder
+ )
+ notificationDataMap[account.uuid] = newNotificationData
+ }
+ }
+
+ return if (operations.isEmpty()) {
+ null
+ } else {
+ RemoveNotificationsResult(
+ notificationData = getNotificationData(account),
+ notificationStoreOperations = operations,
+ notificationHolders = newNotificationHolders,
+ cancelNotificationIds = cancelNotificationIds
+ )
+ }
+ }
+
+ @Synchronized
+ fun clearNotifications(account: Account) {
+ notificationDataMap.remove(account.uuid)
+ }
+
+ private fun getNotificationData(account: Account): NotificationData {
+ return notificationDataMap[account.uuid] ?: NotificationData.create(account).also { notificationData ->
+ notificationDataMap[account.uuid] = notificationData
+ }
+ }
+
+ private val NotificationData.isMaxNumberOfActiveNotificationsReached: Boolean
+ get() = activeNotifications.size == MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS
+
+ private fun NotificationData.getNewNotificationId(): Int {
+ val notificationIdsInUse = activeNotifications.map { it.notificationId }.toSet()
+ for (index in 0 until MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS) {
+ val notificationId = NotificationIds.getSingleMessageNotificationId(account, index)
+ if (notificationId !in notificationIdsInUse) {
+ return notificationId
+ }
+ }
+
+ throw AssertionError("getNewNotificationId() called with no free notification ID")
+ }
+
+ private fun NotificationHolder.toInactiveNotificationHolder() = InactiveNotificationHolder(timestamp, content)
+
+ private fun InactiveNotificationHolder.toNotificationHolder(notificationId: Int): NotificationHolder {
+ return NotificationHolder(notificationId, timestamp, content)
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationGroupKeys.java b/app/core/src/main/java/com/fsck/k9/notification/NotificationGroupKeys.java
deleted file mode 100644
index 7d95f50feba1d58247470108c2691ae3afccf2e5..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationGroupKeys.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import com.fsck.k9.Account;
-
-
-public class NotificationGroupKeys {
- private static final String NOTIFICATION_GROUP_KEY_PREFIX = "newMailNotifications-";
-
-
- public static String getGroupKey(Account account) {
- return NOTIFICATION_GROUP_KEY_PREFIX + account.getAccountNumber();
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationGroupKeys.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationGroupKeys.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1f53762bbc49bfd350541524127cab7e78aa3f62
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationGroupKeys.kt
@@ -0,0 +1,11 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+
+object NotificationGroupKeys {
+ private const val NOTIFICATION_GROUP_KEY_PREFIX = "newMailNotifications-"
+
+ fun getGroupKey(account: Account): String {
+ return NOTIFICATION_GROUP_KEY_PREFIX + account.accountNumber
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationHelper.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationHelper.kt
index 606edc184e65006599dc6703c46eaa0a6454148d..9199fa7ad08dd15ac8700c9c2ff05639d55084b4 100644
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationHelper.kt
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationHelper.kt
@@ -23,6 +23,7 @@ class NotificationHelper(
) {
if (K9.isQuietTime) {
+ builder.setNotificationSilent()
return
}
@@ -34,6 +35,8 @@ class NotificationHelper(
if (vibrationPattern != null) {
builder.setVibrate(vibrationPattern)
}
+ } else {
+ builder.setNotificationSilent()
}
if (ledColor != null) {
@@ -75,8 +78,8 @@ class NotificationHelper(
}
companion object {
- private const val NOTIFICATION_LED_ON_TIME = 500
- private const val NOTIFICATION_LED_OFF_TIME = 2000
+ internal const val NOTIFICATION_LED_ON_TIME = 500
+ internal const val NOTIFICATION_LED_OFF_TIME = 2000
private const val NOTIFICATION_LED_FAST_ON_TIME = 100
private const val NOTIFICATION_LED_FAST_OFF_TIME = 100
@@ -85,3 +88,28 @@ class NotificationHelper(
internal const val NOTIFICATION_LED_FAILURE_COLOR = -0x10000
}
}
+
+internal fun NotificationCompat.Builder.setAppearance(
+ silent: Boolean,
+ appearance: NotificationAppearance
+): NotificationCompat.Builder = apply {
+ if (silent) {
+ setSilent(true)
+ } else {
+ if (!appearance.ringtone.isNullOrEmpty()) {
+ setSound(Uri.parse(appearance.ringtone))
+ }
+
+ if (appearance.vibrationPattern != null) {
+ setVibrate(appearance.vibrationPattern)
+ }
+
+ if (appearance.ledColor != null) {
+ setLights(
+ appearance.ledColor,
+ NotificationHelper.NOTIFICATION_LED_ON_TIME,
+ NotificationHelper.NOTIFICATION_LED_OFF_TIME
+ )
+ }
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationHolder.java b/app/core/src/main/java/com/fsck/k9/notification/NotificationHolder.java
deleted file mode 100644
index b1acf555fbaa4f078a1645a81bfc63ff7eeb8964..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationHolder.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.fsck.k9.notification;
-
-
-class NotificationHolder {
- public final int notificationId;
- public final NotificationContent content;
-
-
- public NotificationHolder(int notificationId, NotificationContent content) {
- this.notificationId = notificationId;
- this.content = content;
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationHolder.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationHolder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..73a3647a18cbb1c7ff27286fad062e066c44cc19
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationHolder.kt
@@ -0,0 +1,12 @@
+package com.fsck.k9.notification
+
+internal data class NotificationHolder(
+ val notificationId: Int,
+ val timestamp: Long,
+ val content: NotificationContent
+)
+
+internal data class InactiveNotificationHolder(
+ val timestamp: Long,
+ val content: NotificationContent
+)
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationIds.java b/app/core/src/main/java/com/fsck/k9/notification/NotificationIds.java
deleted file mode 100644
index e74d7de2f3fd06fbdeec382126b4e2565ab22230..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationIds.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import com.fsck.k9.Account;
-
-
-class NotificationIds {
- public static final int PUSH_NOTIFICATION_ID = 1;
- private static final int NUMBER_OF_GENERAL_NOTIFICATIONS = 1;
-
- private static final int OFFSET_SEND_FAILED_NOTIFICATION = 0;
- private static final int OFFSET_CERTIFICATE_ERROR_INCOMING = 1;
- private static final int OFFSET_CERTIFICATE_ERROR_OUTGOING = 2;
- private static final int OFFSET_AUTHENTICATION_ERROR_INCOMING = 3;
- private static final int OFFSET_AUTHENTICATION_ERROR_OUTGOING = 4;
- private static final int OFFSET_FETCHING_MAIL = 5;
- private static final int OFFSET_NEW_MAIL_SUMMARY = 6;
-
- private static final int OFFSET_NEW_MAIL_STACKED = 7;
-
- private static final int NUMBER_OF_DEVICE_NOTIFICATIONS = 7;
- private static final int NUMBER_OF_STACKED_NOTIFICATIONS = NotificationData.MAX_NUMBER_OF_STACKED_NOTIFICATIONS;
- private static final int NUMBER_OF_NOTIFICATIONS_PER_ACCOUNT = NUMBER_OF_DEVICE_NOTIFICATIONS +
- NUMBER_OF_STACKED_NOTIFICATIONS;
-
-
- public static int getNewMailSummaryNotificationId(Account account) {
- return getBaseNotificationId(account) + OFFSET_NEW_MAIL_SUMMARY;
- }
-
- public static int getNewMailStackedNotificationId(Account account, int index) {
- if (index < 0 || index >= NUMBER_OF_STACKED_NOTIFICATIONS) {
- throw new IndexOutOfBoundsException("Invalid value: " + index);
- }
-
- return getBaseNotificationId(account) + OFFSET_NEW_MAIL_STACKED + index;
- }
-
- public static int getFetchingMailNotificationId(Account account) {
- return getBaseNotificationId(account) + OFFSET_FETCHING_MAIL;
- }
-
- public static int getSendFailedNotificationId(Account account) {
- return getBaseNotificationId(account) + OFFSET_SEND_FAILED_NOTIFICATION;
- }
-
- public static int getCertificateErrorNotificationId(Account account, boolean incoming) {
- int offset = incoming ? OFFSET_CERTIFICATE_ERROR_INCOMING : OFFSET_CERTIFICATE_ERROR_OUTGOING;
- return getBaseNotificationId(account) + offset;
- }
-
- public static int getAuthenticationErrorNotificationId(Account account, boolean incoming) {
- int offset = incoming ? OFFSET_AUTHENTICATION_ERROR_INCOMING : OFFSET_AUTHENTICATION_ERROR_OUTGOING;
- return getBaseNotificationId(account) + offset;
- }
-
- private static int getBaseNotificationId(Account account) {
- return 1 /* skip notification ID 0 */ + NUMBER_OF_GENERAL_NOTIFICATIONS +
- account.getAccountNumber() * NUMBER_OF_NOTIFICATIONS_PER_ACCOUNT;
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationIds.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationIds.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a074c61774b8d3299f890213de8c5e9d9ea7895a
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationIds.kt
@@ -0,0 +1,64 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+
+internal object NotificationIds {
+ const val PUSH_NOTIFICATION_ID = 1
+
+ private const val NUMBER_OF_GENERAL_NOTIFICATIONS = 1
+ private const val OFFSET_SEND_FAILED_NOTIFICATION = 0
+ private const val OFFSET_CERTIFICATE_ERROR_INCOMING = 1
+ private const val OFFSET_CERTIFICATE_ERROR_OUTGOING = 2
+ private const val OFFSET_AUTHENTICATION_ERROR_INCOMING = 3
+ private const val OFFSET_AUTHENTICATION_ERROR_OUTGOING = 4
+ private const val OFFSET_FETCHING_MAIL = 5
+ private const val OFFSET_NEW_MAIL_SUMMARY = 6
+ private const val OFFSET_NEW_MAIL_SINGLE = 7
+ private const val NUMBER_OF_MISC_ACCOUNT_NOTIFICATIONS = 7
+ private const val NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS = MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS
+ private const val NUMBER_OF_NOTIFICATIONS_PER_ACCOUNT =
+ NUMBER_OF_MISC_ACCOUNT_NOTIFICATIONS + NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS
+
+ fun getNewMailSummaryNotificationId(account: Account): Int {
+ return getBaseNotificationId(account) + OFFSET_NEW_MAIL_SUMMARY
+ }
+
+ fun getSingleMessageNotificationId(account: Account, index: Int): Int {
+ require(index in 0 until NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS) { "Invalid index: $index" }
+
+ return getBaseNotificationId(account) + OFFSET_NEW_MAIL_SINGLE + index
+ }
+
+ fun getAllMessageNotificationIds(account: Account): List {
+ val singleMessageNotificationIdRange = (0 until NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS) +
+ (getBaseNotificationId(account) + OFFSET_NEW_MAIL_SINGLE)
+
+ return singleMessageNotificationIdRange.toList() + getNewMailSummaryNotificationId(account)
+ }
+
+ fun getFetchingMailNotificationId(account: Account): Int {
+ return getBaseNotificationId(account) + OFFSET_FETCHING_MAIL
+ }
+
+ fun getSendFailedNotificationId(account: Account): Int {
+ return getBaseNotificationId(account) + OFFSET_SEND_FAILED_NOTIFICATION
+ }
+
+
+ fun getCertificateErrorNotificationId(account: Account, incoming: Boolean): Int {
+ val offset = if (incoming) OFFSET_CERTIFICATE_ERROR_INCOMING else OFFSET_CERTIFICATE_ERROR_OUTGOING
+
+ return getBaseNotificationId(account) + offset
+ }
+
+ fun getAuthenticationErrorNotificationId(account: Account, incoming: Boolean): Int {
+ val offset = if (incoming) OFFSET_AUTHENTICATION_ERROR_INCOMING else OFFSET_AUTHENTICATION_ERROR_OUTGOING
+
+ return getBaseNotificationId(account) + offset
+ }
+
+ private fun getBaseNotificationId(account: Account): Int {
+ return 1 /* skip notification ID 0 */ + NUMBER_OF_GENERAL_NOTIFICATIONS +
+ account.accountNumber * NUMBER_OF_NOTIFICATIONS_PER_ACCOUNT
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..b18f3686aa7259b3179e3482ad0d55fdff644f86
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationRepository.kt
@@ -0,0 +1,111 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.mailstore.LocalStoreProvider
+import com.fsck.k9.mailstore.MessageStoreManager
+
+internal class NotificationRepository(
+ private val notificationStoreProvider: NotificationStoreProvider,
+ private val localStoreProvider: LocalStoreProvider,
+ private val messageStoreManager: MessageStoreManager,
+ private val notificationContentCreator: NotificationContentCreator
+) {
+ private val notificationDataStore = NotificationDataStore()
+
+ @Synchronized
+ fun restoreNotifications(account: Account): NotificationData? {
+ val localStore = localStoreProvider.getInstance(account)
+
+ val (activeNotificationMessages, inactiveNotificationMessages) = localStore.notificationMessages.partition {
+ it.notificationId != null
+ }
+
+ if (activeNotificationMessages.isEmpty()) return null
+
+ val activeNotifications = activeNotificationMessages.map { notificationMessage ->
+ val content = notificationContentCreator.createFromMessage(account, notificationMessage.message)
+ NotificationHolder(notificationMessage.notificationId!!, notificationMessage.timestamp, content)
+ }
+
+ val inactiveNotifications = inactiveNotificationMessages.map { notificationMessage ->
+ val content = notificationContentCreator.createFromMessage(account, notificationMessage.message)
+ InactiveNotificationHolder(notificationMessage.timestamp, content)
+ }
+
+ return notificationDataStore.initializeAccount(account, activeNotifications, inactiveNotifications)
+ }
+
+ @Synchronized
+ fun addNotification(account: Account, content: NotificationContent, timestamp: Long): AddNotificationResult {
+ return notificationDataStore.addNotification(account, content, timestamp).also { result ->
+ persistNotificationDataStoreChanges(account, result.notificationStoreOperations)
+ }
+ }
+
+ @Synchronized
+ fun removeNotifications(
+ account: Account,
+ clearNewMessageState: Boolean = true,
+ selector: (List) -> List
+ ): RemoveNotificationsResult? {
+ return notificationDataStore.removeNotifications(account, selector)?.also { result ->
+ if (clearNewMessageState) {
+ persistNotificationDataStoreChanges(account, result.notificationStoreOperations)
+ }
+ }
+ }
+
+ @Synchronized
+ fun clearNotifications(account: Account, clearNewMessageState: Boolean) {
+ notificationDataStore.clearNotifications(account)
+ clearNotificationStore(account)
+
+ if (clearNewMessageState) {
+ clearNewMessageState(account)
+ }
+ }
+
+ private fun persistNotificationDataStoreChanges(account: Account, operations: List) {
+ val notificationStore = notificationStoreProvider.getNotificationStore(account)
+ notificationStore.persistNotificationChanges(operations)
+
+ setNewMessageState(account, operations)
+ }
+
+ private fun setNewMessageState(account: Account, operations: List) {
+ val messageStore = messageStoreManager.getMessageStore(account)
+
+ for (operation in operations) {
+ when (operation) {
+ is NotificationStoreOperation.Add -> {
+ val messageReference = operation.messageReference
+ messageStore.setNewMessageState(
+ folderId = messageReference.folderId,
+ messageServerId = messageReference.uid,
+ newMessage = true
+ )
+ }
+ is NotificationStoreOperation.Remove -> {
+ val messageReference = operation.messageReference
+ messageStore.setNewMessageState(
+ folderId = messageReference.folderId,
+ messageServerId = messageReference.uid,
+ newMessage = false
+ )
+ }
+ else -> Unit
+ }
+ }
+ }
+
+ private fun clearNewMessageState(account: Account) {
+ val messageStore = messageStoreManager.getMessageStore(account)
+ messageStore.clearNewMessageState()
+ }
+
+ private fun clearNotificationStore(account: Account) {
+ val notificationStore = notificationStoreProvider.getNotificationStore(account)
+ notificationStore.clearNotifications()
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationResourceProvider.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationResourceProvider.kt
index adc9eb8f4d9e9d3d2b80f620eabcce46e2dd1343..e558cd5e74e25f26d33cf12bb235fe5daa5bb4ab 100644
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationResourceProvider.kt
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationResourceProvider.kt
@@ -24,6 +24,7 @@ interface NotificationResourceProvider {
fun authenticationErrorTitle(): String
fun authenticationErrorBody(accountName: String): String
+ fun certificateErrorTitle(): String
fun certificateErrorTitle(accountName: String): String
fun certificateErrorBody(): String
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationStore.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationStore.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4068bad6ffd7036bec6bfd936853b58635bce61b
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationStore.kt
@@ -0,0 +1,6 @@
+package com.fsck.k9.notification
+
+interface NotificationStore {
+ fun persistNotificationChanges(operations: List)
+ fun clearNotifications()
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationStoreOperation.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationStoreOperation.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c03620fba3df31cd01837b0ab59fb938bdb3db74
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationStoreOperation.kt
@@ -0,0 +1,20 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.controller.MessageReference
+
+sealed interface NotificationStoreOperation {
+ data class Add(
+ val messageReference: MessageReference,
+ val notificationId: Int,
+ val timestamp: Long
+ ) : NotificationStoreOperation
+
+ data class Remove(val messageReference: MessageReference) : NotificationStoreOperation
+
+ data class ChangeToInactive(val messageReference: MessageReference) : NotificationStoreOperation
+
+ data class ChangeToActive(
+ val messageReference: MessageReference,
+ val notificationId: Int
+ ) : NotificationStoreOperation
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationStoreProvider.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationStoreProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..44d04096375e6695b4ba4ecd834e9fb720e406ca
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationStoreProvider.kt
@@ -0,0 +1,7 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+
+interface NotificationStoreProvider {
+ fun getNotificationStore(account: Account): NotificationStore
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/PushNotificationManager.kt b/app/core/src/main/java/com/fsck/k9/notification/PushNotificationManager.kt
index c26f7899acf688b66361b2149f464a44ead40183..bd15f47801b7afb7f7d57bbed2db8af025e83ee0 100644
--- a/app/core/src/main/java/com/fsck/k9/notification/PushNotificationManager.kt
+++ b/app/core/src/main/java/com/fsck/k9/notification/PushNotificationManager.kt
@@ -64,6 +64,7 @@ internal class PushNotificationManager(
.setContentIntent(contentIntent)
.setOngoing(true)
.setNotificationSilent()
+ .setPriority(NotificationCompat.PRIORITY_MIN)
.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
.setLocalOnly(true)
.setShowWhen(false)
diff --git a/app/core/src/main/java/com/fsck/k9/notification/RemoveNotificationResult.java b/app/core/src/main/java/com/fsck/k9/notification/RemoveNotificationResult.java
deleted file mode 100644
index 7b57b0442c96d56afbe9d19e0a7262782b003b2c..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/RemoveNotificationResult.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.fsck.k9.notification;
-
-
-class RemoveNotificationResult {
- private final NotificationHolder notificationHolder;
- private final int notificationId;
- private final boolean unknownNotification;
-
-
- private RemoveNotificationResult(NotificationHolder notificationHolder, int notificationId,
- boolean unknownNotification) {
- this.notificationHolder = notificationHolder;
- this.notificationId = notificationId;
- this.unknownNotification = unknownNotification;
- }
-
- public static RemoveNotificationResult createNotification(NotificationHolder notificationHolder) {
- return new RemoveNotificationResult(notificationHolder, notificationHolder.notificationId, false);
- }
-
- public static RemoveNotificationResult cancelNotification(int notificationId) {
- return new RemoveNotificationResult(null, notificationId, false);
- }
-
- public static RemoveNotificationResult unknownNotification() {
- return new RemoveNotificationResult(null, 0, true);
- }
-
- public boolean shouldCreateNotification() {
- return notificationHolder != null;
- }
-
- public int getNotificationId() {
- if (isUnknownNotification()) {
- throw new IllegalStateException("getNotificationId() can only be called when " +
- "isUnknownNotification() returns false");
- }
-
- return notificationId;
- }
-
- public boolean isUnknownNotification() {
- return unknownNotification;
- }
-
- public NotificationHolder getNotificationHolder() {
- if (!shouldCreateNotification()) {
- throw new IllegalStateException("getNotificationHolder() can only be called when " +
- "shouldCreateNotification() returns true");
- }
-
- return notificationHolder;
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/RemoveNotificationsResult.kt b/app/core/src/main/java/com/fsck/k9/notification/RemoveNotificationsResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f3c2d44bffafb34c002104ece3a081cb9bf9a050
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/RemoveNotificationsResult.kt
@@ -0,0 +1,8 @@
+package com.fsck.k9.notification
+
+internal data class RemoveNotificationsResult(
+ val notificationData: NotificationData,
+ val notificationStoreOperations: List,
+ val notificationHolders: List,
+ val cancelNotificationIds: List
+)
diff --git a/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt
new file mode 100644
index 0000000000000000000000000000000000000000..067893a8e2ad1193b3864e6b76e33d892117224f
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt
@@ -0,0 +1,66 @@
+package com.fsck.k9.notification
+
+import android.app.Notification
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import com.fsck.k9.Account
+import com.fsck.k9.helper.ExceptionHelper
+
+internal class SendFailedNotificationController(
+ private val notificationHelper: NotificationHelper,
+ private val actionBuilder: NotificationActionCreator,
+ private val resourceProvider: NotificationResourceProvider
+) {
+ fun showSendFailedNotification(account: Account, exception: Exception) {
+ val title = resourceProvider.sendFailedTitle()
+ val text = ExceptionHelper.getRootCauseMessage(exception)
+
+ val notificationId = NotificationIds.getSendFailedNotificationId(account)
+ val folderListPendingIntent = actionBuilder.createViewFolderListPendingIntent(
+ account, notificationId
+ )
+
+ val notificationBuilder = notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconWarning)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setAutoCancel(true)
+ .setTicker(title)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(folderListPendingIntent)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(text))
+ .setPublicVersion(createLockScreenNotification(account))
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+
+ notificationHelper.configureNotification(
+ builder = notificationBuilder,
+ ringtone = null,
+ vibrationPattern = null,
+ ledColor = NotificationHelper.NOTIFICATION_LED_FAILURE_COLOR,
+ ledSpeed = NotificationHelper.NOTIFICATION_LED_BLINK_FAST,
+ ringAndVibrate = true
+ )
+
+ notificationManager.notify(notificationId, notificationBuilder.build())
+ }
+
+ fun clearSendFailedNotification(account: Account) {
+ val notificationId = NotificationIds.getSendFailedNotificationId(account)
+ notificationManager.cancel(notificationId)
+ }
+
+ private fun createLockScreenNotification(account: Account): Notification {
+ return notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconWarning)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle(resourceProvider.sendFailedTitle())
+ .build()
+ }
+
+ private val notificationManager: NotificationManagerCompat
+ get() = notificationHelper.getNotificationManager()
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotifications.java b/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotifications.java
deleted file mode 100644
index 2bb63cd5e94b79106f7a4968fb59d35d220916a4..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotifications.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.PendingIntent;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.BigTextStyle;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.helper.ExceptionHelper;
-
-import static com.fsck.k9.notification.NotificationHelper.NOTIFICATION_LED_BLINK_FAST;
-import static com.fsck.k9.notification.NotificationHelper.NOTIFICATION_LED_FAILURE_COLOR;
-
-
-class SendFailedNotifications {
- private final NotificationHelper notificationHelper;
- private final NotificationActionCreator actionBuilder;
- private final NotificationResourceProvider resourceProvider;
-
-
- public SendFailedNotifications(NotificationHelper notificationHelper, NotificationActionCreator actionBuilder,
- NotificationResourceProvider resourceProvider) {
- this.notificationHelper = notificationHelper;
- this.actionBuilder = actionBuilder;
- this.resourceProvider = resourceProvider;
- }
-
- public void showSendFailedNotification(Account account, Exception exception) {
- String title = resourceProvider.sendFailedTitle();
- String text = ExceptionHelper.getRootCauseMessage(exception);
-
- int notificationId = NotificationIds.getSendFailedNotificationId(account);
- PendingIntent folderListPendingIntent = actionBuilder.createViewFolderListPendingIntent(
- account, notificationId);
-
- NotificationCompat.Builder builder = notificationHelper
- .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
- .setSmallIcon(resourceProvider.getIconWarning())
- .setWhen(System.currentTimeMillis())
- .setAutoCancel(true)
- .setTicker(title)
- .setContentTitle(title)
- .setContentText(text)
- .setContentIntent(folderListPendingIntent)
- .setStyle(new BigTextStyle().bigText(text))
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setCategory(NotificationCompat.CATEGORY_ERROR);
-
- notificationHelper.configureNotification(builder, null, null, NOTIFICATION_LED_FAILURE_COLOR,
- NOTIFICATION_LED_BLINK_FAST, true);
-
- getNotificationManager().notify(notificationId, builder.build());
- }
-
- public void clearSendFailedNotification(Account account) {
- int notificationId = NotificationIds.getSendFailedNotificationId(account);
- getNotificationManager().cancel(notificationId);
- }
-
- private NotificationManagerCompat getNotificationManager() {
- return notificationHelper.getNotificationManager();
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..46635307a418863fb74749631861464da3d8384b
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt
@@ -0,0 +1,185 @@
+package com.fsck.k9.notification
+
+import android.app.PendingIntent
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.WearableExtender
+import androidx.core.app.NotificationManagerCompat
+import com.fsck.k9.notification.NotificationChannelManager.ChannelType
+import androidx.core.app.NotificationCompat.Builder as NotificationBuilder
+
+internal class SingleMessageNotificationCreator(
+ private val notificationHelper: NotificationHelper,
+ private val actionCreator: NotificationActionCreator,
+ private val resourceProvider: NotificationResourceProvider,
+ private val lockScreenNotificationCreator: LockScreenNotificationCreator,
+ private val notificationManager: NotificationManagerCompat
+) {
+ fun createSingleNotification(
+ baseNotificationData: BaseNotificationData,
+ singleNotificationData: SingleNotificationData,
+ isGroupSummary: Boolean = false
+ ) {
+ val account = baseNotificationData.account
+ val notificationId = singleNotificationData.notificationId
+ val content = singleNotificationData.content
+
+ val notification = notificationHelper.createNotificationBuilder(account, ChannelType.MESSAGES)
+ .setCategory(NotificationCompat.CATEGORY_EMAIL)
+ .setAutoCancel(true)
+ .setGroup(baseNotificationData.groupKey)
+ .setGroupSummary(isGroupSummary)
+ .setSmallIcon(resourceProvider.iconNewMail)
+ .setColor(baseNotificationData.color)
+ .setWhen(singleNotificationData.timestamp)
+ .setTicker(content.summary)
+ .setContentTitle(content.sender)
+ .setContentText(content.subject)
+ .setSubText(baseNotificationData.accountName)
+ .setBigText(content.preview)
+ .setContentIntent(createViewIntent(content, notificationId))
+ .setDeleteIntent(createDismissIntent(content, notificationId))
+ .setDeviceActions(singleNotificationData)
+ .setWearActions(singleNotificationData)
+ .setAppearance(singleNotificationData.isSilent, baseNotificationData.appearance)
+ .setLockScreenNotification(baseNotificationData, singleNotificationData.addLockScreenNotification)
+ .build()
+
+ notificationManager.notify(notificationId, notification)
+ }
+
+ private fun NotificationBuilder.setBigText(text: CharSequence) = apply {
+ setStyle(NotificationCompat.BigTextStyle().bigText(text))
+ }
+
+ private fun createViewIntent(content: NotificationContent, notificationId: Int): PendingIntent {
+ return actionCreator.createViewMessagePendingIntent(content.messageReference, notificationId)
+ }
+
+ private fun createDismissIntent(content: NotificationContent, notificationId: Int): PendingIntent {
+ return actionCreator.createDismissMessagePendingIntent(content.messageReference, notificationId)
+ }
+
+ private fun NotificationBuilder.setDeviceActions(notificationData: SingleNotificationData) = apply {
+ val actions = notificationData.actions
+ for (action in actions) {
+ when (action) {
+ NotificationAction.Reply -> addReplyAction(notificationData)
+ NotificationAction.MarkAsRead -> addMarkAsReadAction(notificationData)
+ NotificationAction.Delete -> addDeleteAction(notificationData)
+ }
+ }
+ }
+
+ private fun NotificationBuilder.addReplyAction(notificationData: SingleNotificationData) {
+ val icon = resourceProvider.iconReply
+ val title = resourceProvider.actionReply()
+ val content = notificationData.content
+ val messageReference = content.messageReference
+ val replyToMessagePendingIntent =
+ actionCreator.createReplyPendingIntent(messageReference, notificationData.notificationId)
+
+ addAction(icon, title, replyToMessagePendingIntent)
+ }
+
+ private fun NotificationBuilder.addMarkAsReadAction(notificationData: SingleNotificationData) {
+ val icon = resourceProvider.iconMarkAsRead
+ val title = resourceProvider.actionMarkAsRead()
+ val content = notificationData.content
+ val notificationId = notificationData.notificationId
+ val messageReference = content.messageReference
+ val action = actionCreator.createMarkMessageAsReadPendingIntent(messageReference, notificationId)
+
+ addAction(icon, title, action)
+ }
+
+ private fun NotificationBuilder.addDeleteAction(notificationData: SingleNotificationData) {
+ val icon = resourceProvider.iconDelete
+ val title = resourceProvider.actionDelete()
+ val content = notificationData.content
+ val notificationId = notificationData.notificationId
+ val messageReference = content.messageReference
+ val action = actionCreator.createDeleteMessagePendingIntent(messageReference, notificationId)
+
+ addAction(icon, title, action)
+ }
+
+ private fun NotificationBuilder.setWearActions(notificationData: SingleNotificationData) = apply {
+ val wearableExtender = WearableExtender().apply {
+ for (action in notificationData.wearActions) {
+ when (action) {
+ WearNotificationAction.Reply -> addReplyAction(notificationData)
+ WearNotificationAction.MarkAsRead -> addMarkAsReadAction(notificationData)
+ WearNotificationAction.Delete -> addDeleteAction(notificationData)
+ WearNotificationAction.Archive -> addArchiveAction(notificationData)
+ WearNotificationAction.Spam -> addMarkAsSpamAction(notificationData)
+ }
+ }
+ }
+
+ extend(wearableExtender)
+ }
+
+ private fun WearableExtender.addReplyAction(notificationData: SingleNotificationData) {
+ val icon = resourceProvider.wearIconReplyAll
+ val title = resourceProvider.actionReply()
+ val messageReference = notificationData.content.messageReference
+ val notificationId = notificationData.notificationId
+ val action = actionCreator.createReplyPendingIntent(messageReference, notificationId)
+ val replyAction = NotificationCompat.Action.Builder(icon, title, action).build()
+
+ addAction(replyAction)
+ }
+
+ private fun WearableExtender.addMarkAsReadAction(notificationData: SingleNotificationData) {
+ val icon = resourceProvider.wearIconMarkAsRead
+ val title = resourceProvider.actionMarkAsRead()
+ val messageReference = notificationData.content.messageReference
+ val notificationId = notificationData.notificationId
+ val action = actionCreator.createMarkMessageAsReadPendingIntent(messageReference, notificationId)
+ val markAsReadAction = NotificationCompat.Action.Builder(icon, title, action).build()
+
+ addAction(markAsReadAction)
+ }
+
+ private fun WearableExtender.addDeleteAction(notificationData: SingleNotificationData) {
+ val icon = resourceProvider.wearIconDelete
+ val title = resourceProvider.actionDelete()
+ val messageReference = notificationData.content.messageReference
+ val notificationId = notificationData.notificationId
+ val action = actionCreator.createDeleteMessagePendingIntent(messageReference, notificationId)
+ val deleteAction = NotificationCompat.Action.Builder(icon, title, action).build()
+
+ addAction(deleteAction)
+ }
+
+ private fun WearableExtender.addArchiveAction(notificationData: SingleNotificationData) {
+ val icon = resourceProvider.wearIconArchive
+ val title = resourceProvider.actionArchive()
+ val messageReference = notificationData.content.messageReference
+ val notificationId = notificationData.notificationId
+ val action = actionCreator.createArchiveMessagePendingIntent(messageReference, notificationId)
+ val archiveAction = NotificationCompat.Action.Builder(icon, title, action).build()
+
+ addAction(archiveAction)
+ }
+
+ private fun WearableExtender.addMarkAsSpamAction(notificationData: SingleNotificationData) {
+ val icon = resourceProvider.wearIconMarkAsSpam
+ val title = resourceProvider.actionMarkAsSpam()
+ val messageReference = notificationData.content.messageReference
+ val notificationId = notificationData.notificationId
+ val action = actionCreator.createMarkMessageAsSpamPendingIntent(messageReference, notificationId)
+ val spamAction = NotificationCompat.Action.Builder(icon, title, action).build()
+
+ addAction(spamAction)
+ }
+
+ private fun NotificationBuilder.setLockScreenNotification(
+ notificationData: BaseNotificationData,
+ addLockScreenNotification: Boolean
+ ) = apply {
+ if (addLockScreenNotification) {
+ lockScreenNotificationCreator.configureLockScreenNotification(this, notificationData)
+ }
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..c9012752fbca9f0dfc46a3341371ff14e311d22c
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationDataCreator.kt
@@ -0,0 +1,89 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.K9
+
+internal class SingleMessageNotificationDataCreator {
+
+ fun createSingleNotificationData(
+ account: Account,
+ notificationId: Int,
+ content: NotificationContent,
+ timestamp: Long,
+ addLockScreenNotification: Boolean
+ ): SingleNotificationData {
+ return SingleNotificationData(
+ notificationId = notificationId,
+ isSilent = true,
+ timestamp = timestamp,
+ content = content,
+ actions = createSingleNotificationActions(),
+ wearActions = createSingleNotificationWearActions(account),
+ addLockScreenNotification = addLockScreenNotification
+ )
+ }
+
+ fun createSummarySingleNotificationData(
+ data: NotificationData,
+ timestamp: Long,
+ silent: Boolean
+ ): SummarySingleNotificationData {
+ return SummarySingleNotificationData(
+ SingleNotificationData(
+ notificationId = NotificationIds.getNewMailSummaryNotificationId(data.account),
+ isSilent = silent,
+ timestamp = timestamp,
+ content = data.activeNotifications.first().content,
+ actions = createSingleNotificationActions(),
+ wearActions = createSingleNotificationWearActions(data.account),
+ addLockScreenNotification = false,
+ ),
+ )
+ }
+
+ @OptIn(ExperimentalStdlibApi::class)
+ private fun createSingleNotificationActions(): List {
+ return buildList {
+ add(NotificationAction.Reply)
+ add(NotificationAction.MarkAsRead)
+
+ if (isDeleteActionEnabled()) {
+ add(NotificationAction.Delete)
+ }
+ }
+ }
+
+ @OptIn(ExperimentalStdlibApi::class)
+ private fun createSingleNotificationWearActions(account: Account): List {
+ return buildList {
+ add(WearNotificationAction.Reply)
+ add(WearNotificationAction.MarkAsRead)
+
+ if (isDeleteActionAvailableForWear()) {
+ add(WearNotificationAction.Delete)
+ }
+
+ if (account.hasArchiveFolder()) {
+ add(WearNotificationAction.Archive)
+ }
+
+ if (isSpamActionAvailableForWear(account)) {
+ add(WearNotificationAction.Spam)
+ }
+ }
+ }
+
+ private fun isDeleteActionEnabled(): Boolean {
+ return K9.notificationQuickDeleteBehaviour != K9.NotificationQuickDelete.NEVER
+ }
+
+ // We don't support confirming actions on Wear devices. So don't show the action when confirmation is enabled.
+ private fun isDeleteActionAvailableForWear(): Boolean {
+ return isDeleteActionEnabled() && !K9.isConfirmDeleteFromNotification
+ }
+
+ // We don't support confirming actions on Wear devices. So don't show the action when confirmation is enabled.
+ private fun isSpamActionAvailableForWear(account: Account): Boolean {
+ return account.hasSpamFolder() && !K9.isConfirmSpam
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d6b85a3aef2bb2e41718d2e71d16fad9fbf7a5cb
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt
@@ -0,0 +1,210 @@
+package com.fsck.k9.notification
+
+import android.app.PendingIntent
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.WearableExtender
+import androidx.core.app.NotificationManagerCompat
+import com.fsck.k9.Account
+import com.fsck.k9.notification.NotificationChannelManager.ChannelType
+import com.fsck.k9.notification.NotificationIds.getNewMailSummaryNotificationId
+import androidx.core.app.NotificationCompat.Builder as NotificationBuilder
+
+internal class SummaryNotificationCreator(
+ private val notificationHelper: NotificationHelper,
+ private val actionCreator: NotificationActionCreator,
+ private val lockScreenNotificationCreator: LockScreenNotificationCreator,
+ private val singleMessageNotificationCreator: SingleMessageNotificationCreator,
+ private val resourceProvider: NotificationResourceProvider,
+ private val notificationManager: NotificationManagerCompat
+) {
+ fun createSummaryNotification(
+ baseNotificationData: BaseNotificationData,
+ summaryNotificationData: SummaryNotificationData
+ ) {
+ when (summaryNotificationData) {
+ is SummarySingleNotificationData -> {
+ createSingleMessageNotification(baseNotificationData, summaryNotificationData.singleNotificationData)
+ }
+ is SummaryInboxNotificationData -> {
+ createInboxStyleSummaryNotification(baseNotificationData, summaryNotificationData)
+ }
+ }
+ }
+
+ private fun createSingleMessageNotification(
+ baseNotificationData: BaseNotificationData,
+ singleNotificationData: SingleNotificationData
+ ) {
+ singleMessageNotificationCreator.createSingleNotification(
+ baseNotificationData,
+ singleNotificationData,
+ isGroupSummary = true
+ )
+ }
+
+ private fun createInboxStyleSummaryNotification(
+ baseNotificationData: BaseNotificationData,
+ notificationData: SummaryInboxNotificationData
+ ) {
+ val account = baseNotificationData.account
+ val accountName = baseNotificationData.accountName
+ val newMessagesCount = baseNotificationData.newMessagesCount
+ val title = resourceProvider.newMessagesTitle(newMessagesCount)
+ val summary = buildInboxSummaryText(accountName, notificationData)
+
+ val notification = notificationHelper.createNotificationBuilder(account, ChannelType.MESSAGES)
+ .setCategory(NotificationCompat.CATEGORY_EMAIL)
+ .setAutoCancel(true)
+ .setGroup(baseNotificationData.groupKey)
+ .setGroupSummary(true)
+ .setSmallIcon(resourceProvider.iconNewMail)
+ .setColor(baseNotificationData.color)
+ .setWhen(notificationData.timestamp)
+ .setNumber(notificationData.additionalMessagesCount)
+ .setTicker(notificationData.content.firstOrNull())
+ .setContentTitle(title)
+ .setSubText(accountName)
+ .setInboxStyle(title, summary, notificationData.content)
+ .setContentIntent(createViewIntent(account, notificationData))
+ .setDeleteIntent(createDismissIntent(account, notificationData.notificationId))
+ .setDeviceActions(account, notificationData)
+ .setWearActions(account, notificationData)
+ .setAppearance(notificationData.isSilent, baseNotificationData.appearance)
+ .setLockScreenNotification(baseNotificationData)
+ .build()
+
+ notificationManager.notify(notificationData.notificationId, notification)
+ }
+
+ private fun buildInboxSummaryText(accountName: String, notificationData: SummaryInboxNotificationData): String {
+ return if (notificationData.additionalMessagesCount > 0) {
+ resourceProvider.additionalMessages(notificationData.additionalMessagesCount, accountName)
+ } else {
+ accountName
+ }
+ }
+
+ private fun NotificationBuilder.setInboxStyle(
+ title: String,
+ summary: String,
+ contentLines: List
+ ) = apply {
+ val style = NotificationCompat.InboxStyle()
+ .setBigContentTitle(title)
+ .setSummaryText(summary)
+
+ for (line in contentLines) {
+ style.addLine(line)
+ }
+
+ setStyle(style)
+ }
+
+ private fun createViewIntent(account: Account, notificationData: SummaryInboxNotificationData): PendingIntent {
+ return actionCreator.createViewMessagesPendingIntent(
+ account = account,
+ messageReferences = notificationData.messageReferences,
+ notificationId = notificationData.notificationId
+ )
+ }
+
+ private fun createDismissIntent(account: Account, notificationId: Int): PendingIntent {
+ return actionCreator.createDismissAllMessagesPendingIntent(account, notificationId)
+ }
+
+ private fun NotificationBuilder.setDeviceActions(
+ account: Account,
+ notificationData: SummaryInboxNotificationData
+ ) = apply {
+ for (action in notificationData.actions) {
+ when (action) {
+ SummaryNotificationAction.MarkAsRead -> addMarkAllAsReadAction(account, notificationData)
+ SummaryNotificationAction.Delete -> addDeleteAllAction(account, notificationData)
+ }
+ }
+ }
+
+ private fun NotificationBuilder.addMarkAllAsReadAction(
+ account: Account,
+ notificationData: SummaryInboxNotificationData
+ ) {
+ val icon = resourceProvider.iconMarkAsRead
+ val title = resourceProvider.actionMarkAsRead()
+ val messageReferences = notificationData.messageReferences
+ val notificationId = notificationData.notificationId
+ val markAllAsReadPendingIntent =
+ actionCreator.createMarkAllAsReadPendingIntent(account, messageReferences, notificationId)
+
+ addAction(icon, title, markAllAsReadPendingIntent)
+ }
+
+ private fun NotificationBuilder.addDeleteAllAction(
+ account: Account,
+ notificationData: SummaryInboxNotificationData
+ ) {
+ val icon = resourceProvider.iconDelete
+ val title = resourceProvider.actionDelete()
+ val notificationId = getNewMailSummaryNotificationId(account)
+ val messageReferences = notificationData.messageReferences
+ val action = actionCreator.createDeleteAllPendingIntent(account, messageReferences, notificationId)
+
+ addAction(icon, title, action)
+ }
+
+ private fun NotificationBuilder.setWearActions(
+ account: Account,
+ notificationData: SummaryInboxNotificationData
+ ) = apply {
+ val wearableExtender = WearableExtender().apply {
+ for (action in notificationData.wearActions) {
+ when (action) {
+ SummaryWearNotificationAction.MarkAsRead -> addMarkAllAsReadAction(account, notificationData)
+ SummaryWearNotificationAction.Delete -> addDeleteAllAction(account, notificationData)
+ SummaryWearNotificationAction.Archive -> addArchiveAllAction(account, notificationData)
+ }
+ }
+ }
+
+ extend(wearableExtender)
+ }
+
+ private fun WearableExtender.addMarkAllAsReadAction(
+ account: Account,
+ notificationData: SummaryInboxNotificationData
+ ) {
+ val icon = resourceProvider.wearIconMarkAsRead
+ val title = resourceProvider.actionMarkAllAsRead()
+ val messageReferences = notificationData.messageReferences
+ val notificationId = getNewMailSummaryNotificationId(account)
+ val action = actionCreator.createMarkAllAsReadPendingIntent(account, messageReferences, notificationId)
+ val markAsReadAction = NotificationCompat.Action.Builder(icon, title, action).build()
+
+ addAction(markAsReadAction)
+ }
+
+ private fun WearableExtender.addDeleteAllAction(account: Account, notificationData: SummaryInboxNotificationData) {
+ val icon = resourceProvider.wearIconDelete
+ val title = resourceProvider.actionDeleteAll()
+ val messageReferences = notificationData.messageReferences
+ val notificationId = getNewMailSummaryNotificationId(account)
+ val action = actionCreator.createDeleteAllPendingIntent(account, messageReferences, notificationId)
+ val deleteAction = NotificationCompat.Action.Builder(icon, title, action).build()
+
+ addAction(deleteAction)
+ }
+
+ private fun WearableExtender.addArchiveAllAction(account: Account, notificationData: SummaryInboxNotificationData) {
+ val icon = resourceProvider.wearIconArchive
+ val title = resourceProvider.actionArchiveAll()
+ val messageReferences = notificationData.messageReferences
+ val notificationId = getNewMailSummaryNotificationId(account)
+ val action = actionCreator.createArchiveAllPendingIntent(account, messageReferences, notificationId)
+ val archiveAction = NotificationCompat.Action.Builder(icon, title, action).build()
+
+ addAction(archiveAction)
+ }
+
+ private fun NotificationBuilder.setLockScreenNotification(notificationData: BaseNotificationData) = apply {
+ lockScreenNotificationCreator.configureLockScreenNotification(this, notificationData)
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..77e1272826515e46aaccca190d9bc1e62f82b56d
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationDataCreator.kt
@@ -0,0 +1,94 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.K9
+
+private const val MAX_NUMBER_OF_MESSAGES_FOR_SUMMARY_NOTIFICATION = 5
+
+internal class SummaryNotificationDataCreator(
+ private val singleMessageNotificationDataCreator: SingleMessageNotificationDataCreator
+) {
+ fun createSummaryNotificationData(data: NotificationData, silent: Boolean): SummaryNotificationData {
+ val timestamp = data.latestTimestamp
+ val shouldBeSilent = silent || K9.isQuietTime
+ return if (data.isSingleMessageNotification) {
+ createSummarySingleNotificationData(data, timestamp, shouldBeSilent)
+ } else {
+ createSummaryInboxNotificationData(data, timestamp, shouldBeSilent)
+ }
+ }
+
+ private fun createSummarySingleNotificationData(
+ data: NotificationData,
+ timestamp: Long,
+ silent: Boolean
+ ): SummaryNotificationData {
+ return singleMessageNotificationDataCreator.createSummarySingleNotificationData(data, timestamp, silent)
+ }
+
+ private fun createSummaryInboxNotificationData(
+ data: NotificationData,
+ timestamp: Long,
+ silent: Boolean
+ ): SummaryNotificationData {
+ return SummaryInboxNotificationData(
+ notificationId = NotificationIds.getNewMailSummaryNotificationId(data.account),
+ isSilent = silent,
+ timestamp = timestamp,
+ content = data.summaryContent,
+ additionalMessagesCount = data.additionalMessagesCount,
+ messageReferences = data.messageReferences,
+ actions = createSummaryNotificationActions(),
+ wearActions = createSummaryWearNotificationActions(data.account)
+ )
+ }
+
+ @OptIn(ExperimentalStdlibApi::class)
+ private fun createSummaryNotificationActions(): List {
+ return buildList {
+ add(SummaryNotificationAction.MarkAsRead)
+
+ if (isDeleteActionEnabled()) {
+ add(SummaryNotificationAction.Delete)
+ }
+ }
+ }
+
+ @OptIn(ExperimentalStdlibApi::class)
+ private fun createSummaryWearNotificationActions(account: Account): List {
+ return buildList {
+ add(SummaryWearNotificationAction.MarkAsRead)
+
+ if (isDeleteActionAvailableForWear()) {
+ add(SummaryWearNotificationAction.Delete)
+ }
+
+ if (account.hasArchiveFolder()) {
+ add(SummaryWearNotificationAction.Archive)
+ }
+ }
+ }
+
+ private fun isDeleteActionEnabled(): Boolean {
+ return K9.notificationQuickDeleteBehaviour == K9.NotificationQuickDelete.ALWAYS
+ }
+
+ // We don't support confirming actions on Wear devices. So don't show the action when confirmation is enabled.
+ private fun isDeleteActionAvailableForWear(): Boolean {
+ return isDeleteActionEnabled() && !K9.isConfirmDeleteFromNotification
+ }
+
+ private val NotificationData.latestTimestamp: Long
+ get() = activeNotifications.first().timestamp
+
+ private val NotificationData.summaryContent: List
+ get() {
+ return activeNotifications.asSequence()
+ .map { it.content.summary }
+ .take(MAX_NUMBER_OF_MESSAGES_FOR_SUMMARY_NOTIFICATION)
+ .toList()
+ }
+
+ private val NotificationData.additionalMessagesCount: Int
+ get() = (newMessagesCount - MAX_NUMBER_OF_MESSAGES_FOR_SUMMARY_NOTIFICATION).coerceAtLeast(0)
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4d4390ef5294504c16beb9925450338d707a4437
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt
@@ -0,0 +1,157 @@
+package com.fsck.k9.notification
+
+import android.app.Notification
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import com.fsck.k9.Account
+import com.fsck.k9.mailstore.LocalFolder
+
+private const val NOTIFICATION_LED_WHILE_SYNCING = false
+
+internal class SyncNotificationController(
+ private val notificationHelper: NotificationHelper,
+ private val actionBuilder: NotificationActionCreator,
+ private val resourceProvider: NotificationResourceProvider
+) {
+ fun showSendingNotification(account: Account) {
+ val accountName = notificationHelper.getAccountName(account)
+ val title = resourceProvider.sendingMailTitle()
+ val tickerText = resourceProvider.sendingMailBody(accountName)
+
+ val notificationId = NotificationIds.getFetchingMailNotificationId(account)
+ val outboxFolderId = account.outboxFolderId ?: error("Outbox folder not configured")
+ val showMessageListPendingIntent = actionBuilder.createViewFolderPendingIntent(
+ account, outboxFolderId, notificationId
+ )
+
+ val notificationBuilder = notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconSendingMail)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setOngoing(true)
+ .setTicker(tickerText)
+ .setContentTitle(title)
+ .setContentText(accountName)
+ .setContentIntent(showMessageListPendingIntent)
+ .setPublicVersion(createSendingLockScreenNotification(account))
+
+ if (NOTIFICATION_LED_WHILE_SYNCING) {
+ notificationHelper.configureNotification(
+ builder = notificationBuilder,
+ ringtone = null,
+ vibrationPattern = null,
+ ledColor = account.notificationSetting.ledColor,
+ ledSpeed = NotificationHelper.NOTIFICATION_LED_BLINK_FAST,
+ ringAndVibrate = true
+ )
+ }
+
+ notificationManager.notify(notificationId, notificationBuilder.build())
+ }
+
+ fun clearSendingNotification(account: Account) {
+ val notificationId = NotificationIds.getFetchingMailNotificationId(account)
+ notificationManager.cancel(notificationId)
+ }
+
+ fun showFetchingMailNotification(account: Account, folder: LocalFolder) {
+ val accountName = account.description
+ val folderId = folder.databaseId
+ val folderName = folder.name
+ val tickerText = resourceProvider.checkingMailTicker(accountName, folderName)
+ val title = resourceProvider.checkingMailTitle()
+
+ // TODO: Use format string from resources
+ val text = accountName + resourceProvider.checkingMailSeparator() + folderName
+
+ val notificationId = NotificationIds.getFetchingMailNotificationId(account)
+ val showMessageListPendingIntent = actionBuilder.createViewFolderPendingIntent(
+ account, folderId, notificationId
+ )
+
+ val notificationBuilder = notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconCheckingMail)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setOngoing(true)
+ .setTicker(tickerText)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(showMessageListPendingIntent)
+ .setPublicVersion(createFetchingMailLockScreenNotification(account))
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
+
+ if (NOTIFICATION_LED_WHILE_SYNCING) {
+ notificationHelper.configureNotification(
+ builder = notificationBuilder,
+ ringtone = null,
+ vibrationPattern = null,
+ ledColor = account.notificationSetting.ledColor,
+ ledSpeed = NotificationHelper.NOTIFICATION_LED_BLINK_FAST,
+ ringAndVibrate = true
+ )
+ }
+
+ notificationManager.notify(notificationId, notificationBuilder.build())
+ }
+
+ fun showEmptyFetchingMailNotification(account: Account) {
+ val title = resourceProvider.checkingMailTitle()
+ val text = account.description
+ val notificationId = NotificationIds.getFetchingMailNotificationId(account)
+
+ val notificationBuilder = notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconCheckingMail)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setOngoing(true)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setPublicVersion(createFetchingMailLockScreenNotification(account))
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
+
+ if (NOTIFICATION_LED_WHILE_SYNCING) {
+ notificationHelper.configureNotification(
+ builder = notificationBuilder,
+ ringtone = null,
+ vibrationPattern = null,
+ ledColor = account.notificationSetting.ledColor,
+ ledSpeed = NotificationHelper.NOTIFICATION_LED_BLINK_FAST,
+ ringAndVibrate = true
+ )
+ }
+
+ notificationManager.notify(notificationId, notificationBuilder.build())
+ }
+
+ fun clearFetchingMailNotification(account: Account) {
+ val notificationId = NotificationIds.getFetchingMailNotificationId(account)
+ notificationManager.cancel(notificationId)
+ }
+
+ private fun createSendingLockScreenNotification(account: Account): Notification {
+ return notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconSendingMail)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle(resourceProvider.sendingMailTitle())
+ .build()
+ }
+
+ private fun createFetchingMailLockScreenNotification(account: Account): Notification {
+ return notificationHelper
+ .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS)
+ .setSmallIcon(resourceProvider.iconCheckingMail)
+ .setColor(account.chipColor)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle(resourceProvider.checkingMailTitle())
+ .build()
+ }
+
+ private val notificationManager: NotificationManagerCompat
+ get() = notificationHelper.getNotificationManager()
+}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/SyncNotifications.java b/app/core/src/main/java/com/fsck/k9/notification/SyncNotifications.java
deleted file mode 100644
index 16a58882034bc982294533464c14507078042407..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/SyncNotifications.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.PendingIntent;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.mailstore.LocalFolder;
-
-import static com.fsck.k9.notification.NotificationHelper.NOTIFICATION_LED_BLINK_FAST;
-
-
-class SyncNotifications {
- private static final boolean NOTIFICATION_LED_WHILE_SYNCING = false;
-
-
- private final NotificationHelper notificationHelper;
- private final NotificationActionCreator actionBuilder;
- private final NotificationResourceProvider resourceProvider;
-
-
- public SyncNotifications(NotificationHelper notificationHelper, NotificationActionCreator actionBuilder,
- NotificationResourceProvider resourceProvider) {
- this.notificationHelper = notificationHelper;
- this.actionBuilder = actionBuilder;
- this.resourceProvider = resourceProvider;
- }
-
- public void showSendingNotification(Account account) {
- String accountName = notificationHelper.getAccountName(account);
- String title = resourceProvider.sendingMailTitle();
- String tickerText = resourceProvider.sendingMailBody(accountName);
-
- int notificationId = NotificationIds.getFetchingMailNotificationId(account);
- long outboxFolderId = account.getOutboxFolderId();
- PendingIntent showMessageListPendingIntent = actionBuilder.createViewFolderPendingIntent(
- account, outboxFolderId, notificationId);
-
- NotificationCompat.Builder builder = notificationHelper.createNotificationBuilder(account,
- NotificationChannelManager.ChannelType.MISCELLANEOUS)
- .setSmallIcon(resourceProvider.getIconSendingMail())
- .setWhen(System.currentTimeMillis())
- .setOngoing(true)
- .setTicker(tickerText)
- .setContentTitle(title)
- .setContentText(accountName)
- .setContentIntent(showMessageListPendingIntent)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
-
- if (NOTIFICATION_LED_WHILE_SYNCING) {
- notificationHelper.configureNotification(builder, null, null,
- account.getNotificationSetting().getLedColor(),
- NOTIFICATION_LED_BLINK_FAST, true);
- }
-
- getNotificationManager().notify(notificationId, builder.build());
- }
-
- public void clearSendingNotification(Account account) {
- int notificationId = NotificationIds.getFetchingMailNotificationId(account);
- getNotificationManager().cancel(notificationId);
- }
-
- public void showFetchingMailNotification(Account account, LocalFolder folder) {
- String accountName = account.getDescription();
- long folderId = folder.getDatabaseId();
- String folderName = folder.getName();
-
- String tickerText = resourceProvider.checkingMailTicker(accountName, folderName);
- String title = resourceProvider.checkingMailTitle();
- //TODO: Use format string from resources
- String text = accountName + resourceProvider.checkingMailSeparator() + folderName;
-
- int notificationId = NotificationIds.getFetchingMailNotificationId(account);
- PendingIntent showMessageListPendingIntent = actionBuilder.createViewFolderPendingIntent(
- account, folderId, notificationId);
-
- NotificationCompat.Builder builder = notificationHelper.createNotificationBuilder(account,
- NotificationChannelManager.ChannelType.MISCELLANEOUS)
- .setSmallIcon(resourceProvider.getIconCheckingMail())
- .setWhen(System.currentTimeMillis())
- .setOngoing(true)
- .setTicker(tickerText)
- .setContentTitle(title)
- .setContentText(text)
- .setContentIntent(showMessageListPendingIntent)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setCategory(NotificationCompat.CATEGORY_SERVICE);
-
- if (NOTIFICATION_LED_WHILE_SYNCING) {
- notificationHelper.configureNotification(builder, null, null,
- account.getNotificationSetting().getLedColor(),
- NOTIFICATION_LED_BLINK_FAST, true);
- }
-
- getNotificationManager().notify(notificationId, builder.build());
- }
-
- public void clearFetchingMailNotification(Account account) {
- int notificationId = NotificationIds.getFetchingMailNotificationId(account);
- getNotificationManager().cancel(notificationId);
- }
-
- private NotificationManagerCompat getNotificationManager() {
- return notificationHelper.getNotificationManager();
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/notification/WearNotifications.java b/app/core/src/main/java/com/fsck/k9/notification/WearNotifications.java
deleted file mode 100644
index 8eba134d5e41c9250ad8962fa200b692cc02f12e..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/notification/WearNotifications.java
+++ /dev/null
@@ -1,253 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.ArrayList;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.Builder;
-import androidx.core.app.NotificationCompat.WearableExtender;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.controller.MessageReference;
-import com.fsck.k9.controller.MessagingController;
-
-
-class WearNotifications extends BaseNotifications {
-
- public WearNotifications(NotificationHelper notificationHelper, NotificationActionCreator actionCreator,
- NotificationResourceProvider resourceProvider) {
- super(notificationHelper, actionCreator, resourceProvider);
- }
-
- public Notification buildStackedNotification(Account account, NotificationHolder holder) {
- int notificationId = holder.notificationId;
- NotificationContent content = holder.content;
- NotificationCompat.Builder builder = createBigTextStyleNotification(account, holder, notificationId);
-
- PendingIntent deletePendingIntent = actionCreator.createDismissMessagePendingIntent(
- context, content.messageReference, holder.notificationId);
- builder.setDeleteIntent(deletePendingIntent);
-
- addActions(builder, account, holder);
-
- return builder.build();
- }
-
-
- public void addSummaryActions(Builder builder, NotificationData notificationData) {
- NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender();
-
- addMarkAllAsReadAction(wearableExtender, notificationData);
-
- if (isDeleteActionAvailableForWear()) {
- addDeleteAllAction(wearableExtender, notificationData);
- }
-
- Account account = notificationData.getAccount();
- if (isArchiveActionAvailableForWear(account)) {
- addArchiveAllAction(wearableExtender, notificationData);
- }
-
- builder.extend(wearableExtender);
- }
-
- private void addMarkAllAsReadAction(WearableExtender wearableExtender, NotificationData notificationData) {
- int icon = resourceProvider.getWearIconMarkAsRead();
- String title = resourceProvider.actionMarkAllAsRead();
-
- Account account = notificationData.getAccount();
- ArrayList messageReferences = notificationData.getAllMessageReferences();
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- PendingIntent action = actionCreator.createMarkAllAsReadPendingIntent(
- account, messageReferences, notificationId);
-
- NotificationCompat.Action markAsReadAction = new NotificationCompat.Action.Builder(icon, title, action).build();
- wearableExtender.addAction(markAsReadAction);
- }
-
- private void addDeleteAllAction(WearableExtender wearableExtender, NotificationData notificationData) {
- int icon = resourceProvider.getWearIconDelete();
- String title = resourceProvider.actionDeleteAll();
-
- Account account = notificationData.getAccount();
- ArrayList messageReferences = notificationData.getAllMessageReferences();
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- PendingIntent action = actionCreator.createDeleteAllPendingIntent(account, messageReferences, notificationId);
-
- NotificationCompat.Action deleteAction = new NotificationCompat.Action.Builder(icon, title, action).build();
- wearableExtender.addAction(deleteAction);
- }
-
- private void addArchiveAllAction(WearableExtender wearableExtender, NotificationData notificationData) {
- int icon = resourceProvider.getWearIconArchive();
- String title = resourceProvider.actionArchiveAll();
-
- Account account = notificationData.getAccount();
- ArrayList messageReferences = notificationData.getAllMessageReferences();
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- PendingIntent action = actionCreator.createArchiveAllPendingIntent(account, messageReferences, notificationId);
-
- NotificationCompat.Action archiveAction = new NotificationCompat.Action.Builder(icon, title, action).build();
- wearableExtender.addAction(archiveAction);
- }
-
- private void addActions(Builder builder, Account account, NotificationHolder holder) {
- addDeviceActions(builder, holder);
- addWearActions(builder, account, holder);
- }
-
- private void addDeviceActions(Builder builder, NotificationHolder holder) {
- addDeviceReplyAction(builder, holder);
- addDeviceMarkAsReadAction(builder, holder);
- addDeviceDeleteAction(builder, holder);
- }
-
- private void addDeviceReplyAction(Builder builder, NotificationHolder holder) {
- int icon = resourceProvider.getIconReply();
- String title = resourceProvider.actionReply();
-
- NotificationContent content = holder.content;
- MessageReference messageReference = content.messageReference;
- PendingIntent replyToMessagePendingIntent =
- actionCreator.createReplyPendingIntent(messageReference, holder.notificationId);
-
- builder.addAction(icon, title, replyToMessagePendingIntent);
- }
-
- private void addDeviceMarkAsReadAction(Builder builder, NotificationHolder holder) {
- int icon = resourceProvider.getIconMarkAsRead();
- String title = resourceProvider.actionMarkAsRead();
-
- NotificationContent content = holder.content;
- int notificationId = holder.notificationId;
- MessageReference messageReference = content.messageReference;
- PendingIntent action = actionCreator.createMarkMessageAsReadPendingIntent(messageReference, notificationId);
-
- builder.addAction(icon, title, action);
- }
-
- private void addDeviceDeleteAction(Builder builder, NotificationHolder holder) {
- if (!isDeleteActionEnabled()) {
- return;
- }
-
- int icon = resourceProvider.getIconDelete();
- String title = resourceProvider.actionDelete();
-
- NotificationContent content = holder.content;
- int notificationId = holder.notificationId;
- MessageReference messageReference = content.messageReference;
- PendingIntent action = actionCreator.createDeleteMessagePendingIntent(messageReference, notificationId);
-
- builder.addAction(icon, title, action);
- }
-
- private void addWearActions(Builder builder, Account account, NotificationHolder holder) {
- NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender();
-
- addReplyAction(wearableExtender, holder);
- addMarkAsReadAction(wearableExtender, holder);
-
- if (isDeleteActionAvailableForWear()) {
- addDeleteAction(wearableExtender, holder);
- }
-
- if (isArchiveActionAvailableForWear(account)) {
- addArchiveAction(wearableExtender, holder);
- }
-
- if (isSpamActionAvailableForWear(account)) {
- addMarkAsSpamAction(wearableExtender, holder);
- }
-
- builder.extend(wearableExtender);
- }
-
- private void addReplyAction(WearableExtender wearableExtender, NotificationHolder holder) {
- int icon = resourceProvider.getWearIconReplyAll();
- String title = resourceProvider.actionReply();
-
- MessageReference messageReference = holder.content.messageReference;
- int notificationId = holder.notificationId;
- PendingIntent action = actionCreator.createReplyPendingIntent(messageReference, notificationId);
-
- NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(icon, title, action).build();
- wearableExtender.addAction(replyAction);
- }
-
- private void addMarkAsReadAction(WearableExtender wearableExtender, NotificationHolder holder) {
- int icon = resourceProvider.getWearIconMarkAsRead();
- String title = resourceProvider.actionMarkAsRead();
-
- MessageReference messageReference = holder.content.messageReference;
- int notificationId = holder.notificationId;
- PendingIntent action = actionCreator.createMarkMessageAsReadPendingIntent(messageReference, notificationId);
-
- NotificationCompat.Action markAsReadAction = new NotificationCompat.Action.Builder(icon, title, action).build();
- wearableExtender.addAction(markAsReadAction);
- }
-
- private void addDeleteAction(WearableExtender wearableExtender, NotificationHolder holder) {
- int icon = resourceProvider.getWearIconDelete();
- String title = resourceProvider.actionDelete();
-
- MessageReference messageReference = holder.content.messageReference;
- int notificationId = holder.notificationId;
- PendingIntent action = actionCreator.createDeleteMessagePendingIntent(messageReference, notificationId);
-
- NotificationCompat.Action deleteAction = new NotificationCompat.Action.Builder(icon, title, action).build();
- wearableExtender.addAction(deleteAction);
- }
-
- private void addArchiveAction(WearableExtender wearableExtender, NotificationHolder holder) {
- int icon = resourceProvider.getWearIconArchive();
- String title = resourceProvider.actionArchive();
-
- MessageReference messageReference = holder.content.messageReference;
- int notificationId = holder.notificationId;
- PendingIntent action = actionCreator.createArchiveMessagePendingIntent(messageReference, notificationId);
-
- NotificationCompat.Action archiveAction = new NotificationCompat.Action.Builder(icon, title, action).build();
- wearableExtender.addAction(archiveAction);
- }
-
- private void addMarkAsSpamAction(WearableExtender wearableExtender, NotificationHolder holder) {
- int icon = resourceProvider.getWearIconMarkAsSpam();
- String title = resourceProvider.actionMarkAsSpam();
-
- MessageReference messageReference = holder.content.messageReference;
- int notificationId = holder.notificationId;
- PendingIntent action = actionCreator.createMarkMessageAsSpamPendingIntent(messageReference, notificationId);
-
- NotificationCompat.Action spamAction = new NotificationCompat.Action.Builder(icon, title, action).build();
- wearableExtender.addAction(spamAction);
- }
-
- private boolean isDeleteActionAvailableForWear() {
- return isDeleteActionEnabled() && !K9.isConfirmDeleteFromNotification();
- }
-
- private boolean isArchiveActionAvailableForWear(Account account) {
- return isMovePossible(account, account.getArchiveFolderId());
- }
-
- private boolean isSpamActionAvailableForWear(Account account) {
- return !K9.isConfirmSpam() && isMovePossible(account, account.getSpamFolderId());
- }
-
- private boolean isMovePossible(Account account, Long destinationFolderId) {
- if (destinationFolderId == null) {
- return false;
- }
-
- MessagingController controller = createMessagingController();
- return controller.isMoveCapable(account);
- }
-
- MessagingController createMessagingController() {
- return MessagingController.getInstance(context);
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/power/AndroidPowerManager.kt b/app/core/src/main/java/com/fsck/k9/power/AndroidPowerManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fbf30c048d2b0b3ce931e008eeabe8158e8ab951
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/power/AndroidPowerManager.kt
@@ -0,0 +1,90 @@
+package com.fsck.k9.power
+
+import android.annotation.SuppressLint
+import android.os.SystemClock
+import com.fsck.k9.mail.power.PowerManager
+import com.fsck.k9.mail.power.WakeLock
+import java.util.concurrent.atomic.AtomicInteger
+import timber.log.Timber
+import android.os.PowerManager as SystemPowerManager
+import android.os.PowerManager.WakeLock as SystemWakeLock
+
+internal class AndroidPowerManager(private val systemPowerManager: SystemPowerManager) : PowerManager {
+ override fun newWakeLock(tag: String): WakeLock {
+ return AndroidWakeLock(SystemPowerManager.PARTIAL_WAKE_LOCK, tag)
+ }
+
+ inner class AndroidWakeLock(flags: Int, val tag: String?) : WakeLock {
+ private val wakeLock: SystemWakeLock = systemPowerManager.newWakeLock(flags, tag)
+ private val id = wakeLockId.getAndIncrement()
+
+ @Volatile
+ private var startTime: Long? = null
+
+ @Volatile
+ private var timeout: Long? = null
+
+ init {
+ Timber.v("AndroidWakeLock for tag %s / id %d: Create", tag, id)
+ }
+
+ override fun acquire(timeout: Long) {
+ synchronized(wakeLock) {
+ wakeLock.acquire(timeout)
+ }
+
+ Timber.v("AndroidWakeLock for tag %s / id %d for %d ms: acquired", tag, id, timeout)
+
+ if (startTime == null) {
+ startTime = SystemClock.elapsedRealtime()
+ }
+
+ this.timeout = timeout
+ }
+
+ @SuppressLint("WakelockTimeout")
+ override fun acquire() {
+ synchronized(wakeLock) {
+ wakeLock.acquire()
+ }
+
+ Timber.v("AndroidWakeLock for tag %s / id %d: acquired with no timeout.", tag, id)
+
+ if (startTime == null) {
+ startTime = SystemClock.elapsedRealtime()
+ }
+
+ timeout = null
+ }
+
+ override fun setReferenceCounted(counted: Boolean) {
+ synchronized(wakeLock) {
+ wakeLock.setReferenceCounted(counted)
+ }
+ }
+
+ override fun release() {
+ val startTime = this.startTime
+ if (startTime != null) {
+ val endTime = SystemClock.elapsedRealtime()
+
+ Timber.v(
+ "AndroidWakeLock for tag %s / id %d: releasing after %d ms, timeout = %d ms",
+ tag, id, endTime - startTime, timeout
+ )
+ } else {
+ Timber.v("AndroidWakeLock for tag %s / id %d, timeout = %d ms: releasing", tag, id, timeout)
+ }
+
+ synchronized(wakeLock) {
+ wakeLock.release()
+ }
+
+ this.startTime = null
+ }
+ }
+
+ companion object {
+ private val wakeLockId = AtomicInteger(0)
+ }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/power/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/power/KoinModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e5e01228bcf618fb703902f54c03f2fb17a22008
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/power/KoinModule.kt
@@ -0,0 +1,10 @@
+package com.fsck.k9.power
+
+import android.content.Context
+import com.fsck.k9.mail.power.PowerManager
+import org.koin.dsl.module
+
+val powerModule = module {
+ factory { get().getSystemService(Context.POWER_SERVICE) as android.os.PowerManager }
+ single { AndroidPowerManager(systemPowerManager = get()) }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/power/TracingPowerManager.java b/app/core/src/main/java/com/fsck/k9/power/TracingPowerManager.java
deleted file mode 100644
index 914311aeabefe02767d31a924e06dd06babb5583..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/power/TracingPowerManager.java
+++ /dev/null
@@ -1,167 +0,0 @@
-package com.fsck.k9.power;
-
-
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import android.content.Context;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.os.SystemClock;
-
-import com.fsck.k9.mail.K9MailLib;
-import org.jetbrains.annotations.NotNull;
-import timber.log.Timber;
-
-
-public class TracingPowerManager implements com.fsck.k9.mail.power.PowerManager {
- private final static boolean TRACE = false;
- public static AtomicInteger wakeLockId = new AtomicInteger(0);
- PowerManager pm = null;
- private static TracingPowerManager tracingPowerManager;
- private Timer timer = null;
-
- public static synchronized TracingPowerManager getPowerManager(Context context) {
- Context appContext = context.getApplicationContext();
- if (tracingPowerManager == null) {
- if (K9MailLib.isDebug()) {
- Timber.v("Creating TracingPowerManager");
- }
- tracingPowerManager = new TracingPowerManager(appContext);
- }
- return tracingPowerManager;
- }
-
-
- private TracingPowerManager(Context context) {
- pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- if (TRACE) {
- timer = new Timer();
- }
- }
-
- @NotNull
- @Override
- public com.fsck.k9.mail.power.WakeLock newWakeLock(@NotNull String tag) {
- return new TracingWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag);
- }
-
- public TracingWakeLock newWakeLock(int flags, String tag) {
- return new TracingWakeLock(flags, tag);
- }
-
-
- public class TracingWakeLock implements com.fsck.k9.mail.power.WakeLock {
- final WakeLock wakeLock;
- final int id;
- final String tag;
- volatile TimerTask timerTask;
- volatile Long startTime = null;
- volatile Long timeout = null;
-
- public TracingWakeLock(int flags, String ntag) {
- tag = ntag;
- wakeLock = pm.newWakeLock(flags, tag);
- id = wakeLockId.getAndIncrement();
- if (K9MailLib.isDebug()) {
- Timber.v("TracingWakeLock for tag %s / id %d: Create", tag, id);
- }
- }
-
- @Override
- public void acquire(long timeout) {
- synchronized (wakeLock) {
- wakeLock.acquire(timeout);
- }
- if (K9MailLib.isDebug()) {
- Timber.v("TracingWakeLock for tag %s / id %d for %d ms: acquired", tag, id, timeout);
- }
- raiseNotification();
- if (startTime == null) {
- startTime = SystemClock.elapsedRealtime();
- }
- this.timeout = timeout;
- }
-
- @Override
- public void acquire() {
- synchronized (wakeLock) {
- wakeLock.acquire();
- }
- raiseNotification();
- if (K9MailLib.isDebug()) {
- Timber.w("TracingWakeLock for tag %s / id %d: acquired with no timeout. K-9 Mail should not do this",
- tag, id);
- }
- if (startTime == null) {
- startTime = SystemClock.elapsedRealtime();
- }
- timeout = null;
- }
-
- @Override
- public void setReferenceCounted(boolean counted) {
- synchronized (wakeLock) {
- wakeLock.setReferenceCounted(counted);
- }
- }
-
- @Override
- public void release() {
- if (startTime != null) {
- Long endTime = SystemClock.elapsedRealtime();
- if (K9MailLib.isDebug()) {
- Timber.v("TracingWakeLock for tag %s / id %d: releasing after %d ms, timeout = %d ms",
- tag, id, endTime - startTime, timeout);
- }
- } else {
- if (K9MailLib.isDebug()) {
- Timber.v("TracingWakeLock for tag %s / id %d, timeout = %d ms: releasing", tag, id, timeout);
- }
- }
- cancelNotification();
- synchronized (wakeLock) {
- wakeLock.release();
- }
- startTime = null;
- }
-
- private void cancelNotification() {
- if (timer != null) {
- synchronized (timer) {
- if (timerTask != null) {
- timerTask.cancel();
- }
- }
- }
- }
-
- private void raiseNotification() {
- if (timer != null) {
- synchronized (timer) {
- if (timerTask != null) {
- timerTask.cancel();
- timerTask = null;
- }
- timerTask = new TimerTask() {
- @Override
- public void run() {
- if (startTime != null) {
- Long endTime = SystemClock.elapsedRealtime();
- Timber.i("TracingWakeLock for tag %s / id %d: has been active for %d ms, timeout = %d ms",
- tag, id, endTime - startTime, timeout);
-
- } else {
- Timber.i("TracingWakeLock for tag %s / id %d: still active, timeout = %d ms",
- tag, id, timeout);
- }
- }
-
- };
- timer.schedule(timerTask, 1000, 1000);
- }
- }
- }
- }
-}
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt b/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt
index d152ab70569e5d306e6d5419f395534c3abad258..5ae56c7e1d235458c7a47d2cc9d204193821e60a 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt
+++ b/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt
@@ -6,6 +6,7 @@ import com.fsck.k9.AccountsChangeListener
import kotlinx.coroutines.flow.Flow
interface AccountManager {
+ fun getAccountsFlow(): Flow>
fun getAccount(accountUuid: String): Account?
fun getAccountFlow(accountUuid: String): Flow
fun addAccountRemovedListener(listener: AccountRemovedListener)
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java b/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java
index f02acea45248e46f748a69448aecb8375a83a730..04f2df62ce2682017cff0121b9367c220e7057e0 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java
+++ b/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java
@@ -60,7 +60,8 @@ public class AccountSettingsDescriptions {
new V(53, new StringSetting(null))
));
s.put("autoExpandFolderName", Settings.versions(
- new V(1, new StringSetting("INBOX"))
+ new V(1, new StringSetting("INBOX")),
+ new V(78, new StringSetting(null))
));
s.put("automaticCheckIntervalMinutes", Settings.versions(
new V(1, new IntegerResourceSetting(-1, R.array.check_frequency_values)),
@@ -216,7 +217,7 @@ public class AccountSettingsDescriptions {
new V(1, new IntegerResourceSetting(0, R.array.vibrate_pattern_values))
));
s.put("vibrateTimes", Settings.versions(
- new V(1, new IntegerResourceSetting(5, R.array.vibrate_times_label))
+ new V(1, new IntegerRangeSetting(1, 10, 5))
));
s.put("allowRemoteSearch", Settings.versions(
new V(18, new BooleanSetting(true))
@@ -261,6 +262,9 @@ public class AccountSettingsDescriptions {
s.put("trashFolderSelection", Settings.versions(
new V(54, new EnumSetting<>(SpecialFolderSelection.class, SpecialFolderSelection.AUTOMATIC))
));
+ s.put("ignoreChatMessages", Settings.versions(
+ new V(76, new BooleanSetting(false))
+ ));
// note that there is no setting for openPgpProvider, because this will have to be set up together
// with the actual provider after import anyways.
@@ -410,8 +414,8 @@ public class AccountSettingsDescriptions {
@Override
public String fromString(String value) {
StorageManager storageManager = StorageManager.getInstance(context);
- Map providers = storageManager.getAvailableProviders();
- if (providers.containsKey(value)) {
+ Set providers = storageManager.getAvailableProviders();
+ if (providers.contains(value)) {
return value;
}
throw new RuntimeException("Validation failed");
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/FolderSettingsProvider.kt b/app/core/src/main/java/com/fsck/k9/preferences/FolderSettingsProvider.kt
index 8485a0752fbc7efa0fd045643e2004ab6249885e..559a4ceb06453c1c6e1824a061c83c0861e83349 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/FolderSettingsProvider.kt
+++ b/app/core/src/main/java/com/fsck/k9/preferences/FolderSettingsProvider.kt
@@ -2,13 +2,12 @@ package com.fsck.k9.preferences
import com.fsck.k9.Account
import com.fsck.k9.mail.FolderClass
-import com.fsck.k9.mailstore.FolderRepositoryManager
+import com.fsck.k9.mailstore.FolderRepository
import com.fsck.k9.mailstore.RemoteFolderDetails
-class FolderSettingsProvider(private val folderRepositoryManager: FolderRepositoryManager) {
+class FolderSettingsProvider(private val folderRepository: FolderRepository) {
fun getFolderSettings(account: Account): List {
- val folderRepository = folderRepositoryManager.getFolderRepository(account)
- return folderRepository.getRemoteFolderDetails()
+ return folderRepository.getRemoteFolderDetails(account)
.filterNot { it.containsOnlyDefaultValues() }
.map { it.toFolderSettings() }
}
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettings.kt b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettings.kt
index cd486f974015de1b495f22ecef85a20bbabbfa84..927f04afa9bc1503692e57152ca16be308cc9575 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettings.kt
+++ b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettings.kt
@@ -12,7 +12,11 @@ package com.fsck.k9.preferences
// TODO: Move over settings from K9
data class GeneralSettings(
val backgroundSync: BackgroundSync,
- val showRecentChanges: Boolean
+ val showRecentChanges: Boolean,
+ val appTheme: AppTheme,
+ val messageViewTheme: SubTheme,
+ val messageComposeTheme: SubTheme,
+ val fixedMessageViewTheme: Boolean
)
enum class BackgroundSync {
@@ -20,3 +24,15 @@ enum class BackgroundSync {
NEVER,
FOLLOW_SYSTEM_AUTO_SYNC
}
+
+enum class AppTheme {
+ LIGHT,
+ DARK,
+ FOLLOW_SYSTEM
+}
+
+enum class SubTheme {
+ LIGHT,
+ DARK,
+ USE_GLOBAL
+}
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java
index d0cd53220475dd3110926db64a3f0589439f312a..7131e174160c804dd9e185d60601cdf15a0ba2c2 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java
+++ b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java
@@ -16,11 +16,8 @@ import com.fsck.k9.Account.SortType;
import com.fsck.k9.DI;
import com.fsck.k9.FontSizes;
import com.fsck.k9.K9;
-import com.fsck.k9.K9.NotificationHideSubject;
import com.fsck.k9.K9.NotificationQuickDelete;
import com.fsck.k9.K9.SplitViewMode;
-import com.fsck.k9.K9.AppTheme;
-import com.fsck.k9.K9.SubTheme;
import com.fsck.k9.core.R;
import com.fsck.k9.preferences.Settings.BooleanSetting;
import com.fsck.k9.preferences.Settings.ColorSetting;
@@ -132,10 +129,6 @@ public class GeneralSettingsDescriptions {
new V(1, new BooleanSetting(false)),
new V(69, null)
));
- s.put("keyguardPrivacy", Settings.versions(
- new V(1, new BooleanSetting(false)),
- new V(12, null)
- ));
s.put("language", Settings.versions(
new V(1, new LanguageSetting())
));
@@ -195,9 +188,6 @@ public class GeneralSettingsDescriptions {
s.put("useVolumeKeysForNavigation", Settings.versions(
new V(1, new BooleanSetting(false))
));
- s.put("notificationHideSubject", Settings.versions(
- new V(12, new EnumSetting<>(NotificationHideSubject.class, NotificationHideSubject.NEVER))
- ));
s.put("useBackgroundAsUnreadIndicator", Settings.versions(
new V(19, new BooleanSetting(true)),
new V(59, new BooleanSetting(false))
@@ -283,11 +273,13 @@ public class GeneralSettingsDescriptions {
s.put("showRecentChanges", Settings.versions(
new V(73, new BooleanSetting(true))
));
+ s.put("showStarredCount", Settings.versions(
+ new V(75, new BooleanSetting(false))
+ ));
SETTINGS = Collections.unmodifiableMap(s);
Map u = new HashMap<>();
- u.put(12, new SettingsUpgraderV12());
u.put(24, new SettingsUpgraderV24());
u.put(31, new SettingsUpgraderV31());
u.put(58, new SettingsUpgraderV58());
@@ -319,27 +311,6 @@ public class GeneralSettingsDescriptions {
return result;
}
- /**
- * Upgrades the settings from version 11 to 12
- *
- * Map the 'keyguardPrivacy' value to the new NotificationHideSubject enum.
- */
- private static class SettingsUpgraderV12 implements SettingsUpgrader {
-
- @Override
- public Set upgrade(Map settings) {
- Boolean keyguardPrivacy = (Boolean) settings.get("keyguardPrivacy");
- if (keyguardPrivacy != null && keyguardPrivacy) {
- // current setting: only show subject when unlocked
- settings.put("notificationHideSubject", NotificationHideSubject.WHEN_LOCKED);
- } else {
- // always show subject [old default]
- settings.put("notificationHideSubject", NotificationHideSubject.NEVER);
- }
- return new HashSet<>(Collections.singletonList("keyguardPrivacy"));
- }
- }
-
/**
* Upgrades the settings from version 23 to 24.
*
@@ -490,7 +461,7 @@ public class GeneralSettingsDescriptions {
@Override
public AppTheme fromString(String value) throws InvalidSettingValueException {
try {
- return K9.AppTheme.valueOf(value);
+ return AppTheme.valueOf(value);
} catch (IllegalArgumentException e) {
throw new InvalidSettingValueException();
}
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsManager.kt b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsManager.kt
index b0fc3c097ab6aec533f5118531363865c777e5a8..2c9c4e2eaeb7f45d7cf987f24e9a9295f0e99f95 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsManager.kt
+++ b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsManager.kt
@@ -12,4 +12,8 @@ interface GeneralSettingsManager {
fun getSettingsFlow(): Flow
fun setShowRecentChanges(showRecentChanges: Boolean)
+ fun setAppTheme(appTheme: AppTheme)
+ fun setMessageViewTheme(subTheme: SubTheme)
+ fun setMessageComposeTheme(subTheme: SubTheme)
+ fun setFixedMessageViewTheme(fixedMessageViewTheme: Boolean)
}
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 29acdbdb2a6c472872e9a55593d440b5acc18fca..8d1ca4c88a633ac335888327a37734678521a2b3 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,10 +11,10 @@ val preferencesModule = module {
contentResolver = get(),
preferences = get(),
folderSettingsProvider = get(),
- folderRepositoryManager = get()
+ folderRepository = get()
)
}
- factory { FolderSettingsProvider(folderRepositoryManager = get()) }
+ factory { FolderSettingsProvider(folderRepository = get()) }
factory { get() }
single {
RealGeneralSettingsManager(
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/RealGeneralSettingsManager.kt b/app/core/src/main/java/com/fsck/k9/preferences/RealGeneralSettingsManager.kt
index b3a17c3e88344460a41196c4b19f68a0f632898e..93b5ed8699d4d550b59d908f9ab19c3ef35a27d6 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/RealGeneralSettingsManager.kt
+++ b/app/core/src/main/java/com/fsck/k9/preferences/RealGeneralSettingsManager.kt
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
+import timber.log.Timber
/**
* Retrieve and modify general settings.
@@ -99,8 +100,32 @@ internal class RealGeneralSettingsManager(
getSettings().copy(showRecentChanges = showRecentChanges).persist()
}
+ @Synchronized
+ override fun setAppTheme(appTheme: AppTheme) {
+ getSettings().copy(appTheme = appTheme).persist()
+ }
+
+ @Synchronized
+ override fun setMessageViewTheme(subTheme: SubTheme) {
+ getSettings().copy(messageViewTheme = subTheme).persist()
+ }
+
+ @Synchronized
+ override fun setMessageComposeTheme(subTheme: SubTheme) {
+ getSettings().copy(messageComposeTheme = subTheme).persist()
+ }
+
+ @Synchronized
+ override fun setFixedMessageViewTheme(fixedMessageViewTheme: Boolean) {
+ getSettings().copy(fixedMessageViewTheme = fixedMessageViewTheme).persist()
+ }
+
private fun writeSettings(editor: StorageEditor, settings: GeneralSettings) {
editor.putBoolean("showRecentChanges", settings.showRecentChanges)
+ editor.putEnum("theme", settings.appTheme)
+ editor.putEnum("messageViewTheme", settings.messageViewTheme)
+ editor.putEnum("messageComposeTheme", settings.messageComposeTheme)
+ editor.putBoolean("fixedMessageViewTheme", settings.fixedMessageViewTheme)
}
private fun loadGeneralSettings(): GeneralSettings {
@@ -108,7 +133,11 @@ internal class RealGeneralSettingsManager(
val settings = GeneralSettings(
backgroundSync = K9.backgroundOps.toBackgroundSync(),
- showRecentChanges = storage.getBoolean("showRecentChanges", true)
+ showRecentChanges = storage.getBoolean("showRecentChanges", true),
+ appTheme = storage.getEnum("theme", AppTheme.FOLLOW_SYSTEM),
+ messageViewTheme = storage.getEnum("messageViewTheme", SubTheme.USE_GLOBAL),
+ messageComposeTheme = storage.getEnum("messageComposeTheme", SubTheme.USE_GLOBAL),
+ fixedMessageViewTheme = storage.getBoolean("fixedMessageViewTheme", true)
)
updateSettingsFlow(settings)
@@ -124,3 +153,21 @@ private fun K9.BACKGROUND_OPS.toBackgroundSync(): BackgroundSync {
K9.BACKGROUND_OPS.WHEN_CHECKED_AUTO_SYNC -> BackgroundSync.FOLLOW_SYSTEM_AUTO_SYNC
}
}
+
+private inline fun > Storage.getEnum(key: String, defaultValue: T): T {
+ return try {
+ val value = getString(key, null)
+ if (value != null) {
+ enumValueOf(value)
+ } else {
+ defaultValue
+ }
+ } catch (e: Exception) {
+ Timber.e("Couldn't read setting '%s'. Using default value instead.", key)
+ defaultValue
+ }
+}
+
+private fun > StorageEditor.putEnum(key: String, value: T) {
+ putString(key, value.name)
+}
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/ServerTypeConverter.kt b/app/core/src/main/java/com/fsck/k9/preferences/ServerTypeConverter.kt
index d1bedc6c8006e6f129ce84fe2fdb3bd48aaa8505..9d64def8b3dbe7be3f969c19572faa67accc43d6 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/ServerTypeConverter.kt
+++ b/app/core/src/main/java/com/fsck/k9/preferences/ServerTypeConverter.kt
@@ -1,10 +1,8 @@
package com.fsck.k9.preferences
-import java.util.Locale
-
object ServerTypeConverter {
@JvmStatic
- fun toServerSettingsType(exportType: String): String = exportType.toLowerCase(Locale.ROOT)
+ fun toServerSettingsType(exportType: String): String = exportType.lowercase()
@JvmStatic
fun fromServerSettingsType(serverSettingsType: String): String = when (serverSettingsType) {
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/Settings.java b/app/core/src/main/java/com/fsck/k9/preferences/Settings.java
index 08ace7bc54f9f50aa597f96b77bbd3832e2b0383..67205834947cf687b63ce97055e3ca4a89f81309 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/Settings.java
+++ b/app/core/src/main/java/com/fsck/k9/preferences/Settings.java
@@ -36,7 +36,7 @@ public class Settings {
*
* @see SettingsExporter
*/
- public static final int VERSION = 74;
+ public static final int VERSION = 78;
static Map validate(int version, Map> settings,
Map importedSettings, boolean useDefaultValues) {
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 8fcdb6063fa02c8d5340f1905d685cc6956427c8..0e879125dfbd519aeace8aa0016d4f8e1b08b963 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,13 +10,13 @@ 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.mailstore.FolderRepositoryManager
import com.fsck.k9.preferences.ServerTypeConverter.fromServerSettingsType
import com.fsck.k9.preferences.Settings.InvalidSettingValueException
import com.fsck.k9.preferences.Settings.SettingsDescription
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Calendar
+import java.util.Locale
import org.xmlpull.v1.XmlSerializer
import timber.log.Timber
@@ -24,7 +24,7 @@ class SettingsExporter(
private val contentResolver: ContentResolver,
private val preferences: Preferences,
private val folderSettingsProvider: FolderSettingsProvider,
- private val folderRepositoryManager: FolderRepositoryManager
+ private val folderRepository: FolderRepository
) {
@Throws(SettingsImportExportException::class)
fun exportToUri(includeGlobals: Boolean, accountUuids: Set, uri: Uri) {
@@ -211,7 +211,6 @@ class SettingsExporter(
}
}
- val folderRepository = folderRepositoryManager.getFolderRepository(account)
writeFolderNameSettings(account, folderRepository, serializer)
serializer.endTag(null, SETTINGS_ELEMENT)
@@ -270,17 +269,29 @@ class SettingsExporter(
folderRepository: FolderRepository,
serializer: XmlSerializer
) {
- fun writeFolderNameSetting(key: String, folderId: Long?, importedFolderServerId: String?) {
+ fun writeFolderNameSetting(
+ key: String,
+ folderId: Long?,
+ importedFolderServerId: String?,
+ writeEmptyValue: Boolean = false
+ ) {
val folderServerId = folderId?.let {
- folderRepository.getFolderServerId(folderId)
+ folderRepository.getFolderServerId(account, folderId)
} ?: importedFolderServerId
if (folderServerId != null) {
writeAccountSettingIfValid(serializer, key, folderServerId, account)
+ } else if (writeEmptyValue) {
+ writeAccountSettingIfValid(serializer, key, valueString = "", account)
}
}
- writeFolderNameSetting("autoExpandFolderName", account.autoExpandFolderId, account.importedAutoExpandFolder)
+ writeFolderNameSetting(
+ "autoExpandFolderName",
+ account.autoExpandFolderId,
+ account.importedAutoExpandFolder,
+ writeEmptyValue = true
+ )
writeFolderNameSetting("archiveFolderName", account.archiveFolderId, account.importedArchiveFolder)
writeFolderNameSetting("draftsFolderName", account.draftsFolderId, account.importedDraftsFolder)
writeFolderNameSetting("sentFolderName", account.sentFolderId, account.importedSentFolder)
@@ -435,7 +446,7 @@ class SettingsExporter(
fun generateDatedExportFileName(): String {
val now = Calendar.getInstance()
- val dateFormat = SimpleDateFormat("yyyy-MM-dd")
+ val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
return String.format("%s_%s.%s", EXPORT_FILENAME_PREFIX, dateFormat.format(now.time), EXPORT_FILENAME_SUFFIX)
}
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java b/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java
index 67a9e875f8eaeedfdcdead71c86dfd86ca3dac1b..e07208a1679e276680daff214da8ebd56df3b7c7 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java
+++ b/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java
@@ -266,11 +266,6 @@ public class SettingsImporter {
StorageEditor editor = preferences.createStorageEditor();
- String defaultAccountUuid = storage.getString("defaultAccountUuid", null);
- if (defaultAccountUuid == null) {
- putString(editor, "defaultAccountUuid", accountUuids.get(0));
- }
-
if (!editor.commit()) {
throw new SettingsImportExportException("Failed to set default account");
}
diff --git a/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java b/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java
index f5ab06d99d1a89af8d2d0c40dd97ef986b242d0d..f24173dbe59697cba1c612956b60a91b7c7c5ae7 100644
--- a/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java
+++ b/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java
@@ -27,8 +27,6 @@ import com.fsck.k9.mailstore.LocalStore;
import com.fsck.k9.mailstore.LocalStoreProvider;
import com.fsck.k9.mailstore.LockableDatabase;
import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
-import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
-import com.fsck.k9.mailstore.UnavailableStorageException;
import com.fsck.k9.search.SqlQueryBuilder;
@@ -277,8 +275,7 @@ public class EmailProvider extends ContentProvider {
try {
return database.execute(false, new DbCallback() {
@Override
- public Cursor doDbWork(SQLiteDatabase db) throws WrappedException,
- UnavailableStorageException {
+ public Cursor doDbWork(SQLiteDatabase db) {
String where;
if (TextUtils.isEmpty(selection)) {
@@ -326,8 +323,6 @@ public class EmailProvider extends ContentProvider {
return cursor;
}
});
- } catch (UnavailableStorageException e) {
- throw new RuntimeException("Storage not available", e);
} catch (MessagingException e) {
throw new RuntimeException("messaging exception", e);
}
@@ -342,8 +337,7 @@ public class EmailProvider extends ContentProvider {
try {
return database.execute(false, new DbCallback() {
@Override
- public Cursor doDbWork(SQLiteDatabase db) throws WrappedException,
- UnavailableStorageException {
+ public Cursor doDbWork(SQLiteDatabase db) {
StringBuilder query = new StringBuilder();
@@ -401,8 +395,6 @@ public class EmailProvider extends ContentProvider {
return db.rawQuery(query.toString(), selectionArgs);
}
});
- } catch (UnavailableStorageException e) {
- throw new RuntimeException("Storage not available", e);
} catch (MessagingException e) {
throw new RuntimeException("messaging exception", e);
}
@@ -469,8 +461,7 @@ public class EmailProvider extends ContentProvider {
try {
return database.execute(false, new DbCallback() {
@Override
- public Cursor doDbWork(SQLiteDatabase db) throws WrappedException,
- UnavailableStorageException {
+ public Cursor doDbWork(SQLiteDatabase db) {
StringBuilder query = new StringBuilder();
query.append("SELECT ");
@@ -507,8 +498,6 @@ public class EmailProvider extends ContentProvider {
return db.rawQuery(query.toString(), new String[] { threadId });
}
});
- } catch (UnavailableStorageException e) {
- throw new RuntimeException("Storage not available", e);
} catch (MessagingException e) {
throw new RuntimeException("messaging exception", e);
}
diff --git a/app/core/src/main/java/com/fsck/k9/provider/RawMessageProvider.java b/app/core/src/main/java/com/fsck/k9/provider/RawMessageProvider.java
index 1f80af69d61c7317b2ab3afe9b611bee86a5a2ac..37fcfddeb1a9da163ff2edcfc33569231bc6e7fd 100644
--- a/app/core/src/main/java/com/fsck/k9/provider/RawMessageProvider.java
+++ b/app/core/src/main/java/com/fsck/k9/provider/RawMessageProvider.java
@@ -99,8 +99,7 @@ public class RawMessageProvider extends ContentProvider {
private long computeMessageSize(LocalMessage message) {
// TODO: Store message size in database when saving message so this can be a simple lookup instead.
- try {
- CountingOutputStream countingOutputStream = new CountingOutputStream();
+ try (CountingOutputStream countingOutputStream = new CountingOutputStream()) {
message.writeTo(countingOutputStream);
return countingOutputStream.getCount();
} catch (IOException | MessagingException e) {
diff --git a/app/core/src/main/java/com/fsck/k9/search/LocalSearchExtensions.kt b/app/core/src/main/java/com/fsck/k9/search/LocalSearchExtensions.kt
index 8e49538dac515b1df574255d2cf10c66b80ae599..d528446987b95e18cdca516aeae79c9d943a4373 100644
--- a/app/core/src/main/java/com/fsck/k9/search/LocalSearchExtensions.kt
+++ b/app/core/src/main/java/com/fsck/k9/search/LocalSearchExtensions.kt
@@ -5,6 +5,18 @@ package com.fsck.k9.search
import com.fsck.k9.Account
import com.fsck.k9.Preferences
+val LocalSearch.isUnifiedInbox: Boolean
+ get() = id == SearchAccount.UNIFIED_INBOX
+
+val LocalSearch.isNewMessages: Boolean
+ get() = id == SearchAccount.NEW_MESSAGES
+
+val LocalSearch.isSingleAccount: Boolean
+ get() = accountUuids.size == 1
+
+val LocalSearch.isSingleFolder: Boolean
+ get() = isSingleAccount && folderIds.size == 1
+
@JvmName("getAccountsFromLocalSearch")
fun LocalSearch.getAccounts(preferences: Preferences): List {
val accounts = preferences.accounts
diff --git a/app/core/src/main/java/com/fsck/k9/search/SearchAccount.java b/app/core/src/main/java/com/fsck/k9/search/SearchAccount.java
index 53a8066d3ba822e157bb54d5e89090ef499992f7..b5ccbacb30d126529af5fed8a8546944a412af1c 100644
--- a/app/core/src/main/java/com/fsck/k9/search/SearchAccount.java
+++ b/app/core/src/main/java/com/fsck/k9/search/SearchAccount.java
@@ -14,6 +14,7 @@ import com.fsck.k9.search.SearchSpecification.SearchField;
*/
public class SearchAccount implements BaseAccount {
public static final String UNIFIED_INBOX = "unified_inbox";
+ public static final String NEW_MESSAGES = "new_messages";
// create the unified inbox meta account ( all accounts is default when none specified )
diff --git a/app/core/src/main/java/com/fsck/k9/search/SearchSpecification.java b/app/core/src/main/java/com/fsck/k9/search/SearchSpecification.java
index 7ca698844c3278ef7d123c5b5ebfe97a1888d4dd..f97875167f59cd1e10d097a4c9ebca1f6f86a0db 100644
--- a/app/core/src/main/java/com/fsck/k9/search/SearchSpecification.java
+++ b/app/core/src/main/java/com/fsck/k9/search/SearchSpecification.java
@@ -68,6 +68,7 @@ public interface SearchSpecification extends Parcelable {
THREAD_ID,
ID,
INTEGRATE,
+ NEW_MESSAGE,
READ,
FLAGGED,
DISPLAY_CLASS,
diff --git a/app/core/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java b/app/core/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java
index 1b70f491d274b9a8411dab130d1bc08a5d2be653..06b14bef874ed817aae3bc7679f164a2951ea6d5 100644
--- a/app/core/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java
+++ b/app/core/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java
@@ -150,6 +150,10 @@ public class SqlQueryBuilder {
columnName = "integrate";
break;
}
+ case NEW_MESSAGE: {
+ columnName = "new_message";
+ break;
+ }
case READ: {
columnName = "read";
break;
@@ -247,6 +251,7 @@ public class SqlQueryBuilder {
case FOLDER:
case ID:
case INTEGRATE:
+ case NEW_MESSAGE:
case THREAD_ID:
case READ:
case FLAGGED: {
diff --git a/app/core/src/main/java/com/fsck/k9/service/DatabaseUpgradeService.java b/app/core/src/main/java/com/fsck/k9/service/DatabaseUpgradeService.java
index 5aa195aaa0bc3a5a7bfb019c5d4cba035e214255..efc8bf620876e1df7d44a074ce983d04e54a91c0 100644
--- a/app/core/src/main/java/com/fsck/k9/service/DatabaseUpgradeService.java
+++ b/app/core/src/main/java/com/fsck/k9/service/DatabaseUpgradeService.java
@@ -8,17 +8,15 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
-import android.os.PowerManager;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.fsck.k9.Account;
import com.fsck.k9.DI;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
+import com.fsck.k9.mail.power.PowerManager;
+import com.fsck.k9.mail.power.WakeLock;
import com.fsck.k9.mailstore.LocalStoreProvider;
-import com.fsck.k9.mailstore.UnavailableStorageException;
-import com.fsck.k9.power.TracingPowerManager;
-import com.fsck.k9.power.TracingPowerManager.TracingWakeLock;
import timber.log.Timber;
/**
@@ -103,7 +101,7 @@ public class DatabaseUpgradeService extends Service {
private int mProgress;
private int mProgressEnd;
- private TracingWakeLock mWakeLock;
+ private WakeLock mWakeLock;
@Override
@@ -140,8 +138,8 @@ public class DatabaseUpgradeService extends Service {
* Acquire a partial wake lock so the CPU won't go to sleep when the screen is turned off.
*/
private void acquireWakelock() {
- TracingPowerManager pm = TracingPowerManager.getPowerManager(this);
- mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
+ PowerManager pm = DI.get(PowerManager.class);
+ mWakeLock = pm.newWakeLock(WAKELOCK_TAG);
mWakeLock.setReferenceCounted(false);
mWakeLock.acquire(WAKELOCK_TIMEOUT);
}
@@ -195,8 +193,6 @@ public class DatabaseUpgradeService extends Service {
try {
// Account.getLocalStore() is blocking and will upgrade the database if necessary
DI.get(LocalStoreProvider.class).getInstance(account);
- } catch (UnavailableStorageException e) {
- Timber.e("Database unavailable");
} catch (Exception e) {
Timber.e(e, "Error while upgrading database");
}
diff --git a/app/core/src/main/java/com/fsck/k9/service/StorageGoneReceiver.java b/app/core/src/main/java/com/fsck/k9/service/StorageGoneReceiver.java
deleted file mode 100644
index e103fdf416c97e54f691a1bbb936be41fa3354d4..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/service/StorageGoneReceiver.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.fsck.k9.service;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import timber.log.Timber;
-
-import com.fsck.k9.mailstore.StorageManager;
-
-/**
- * That BroadcastReceiver is only interested in UNMOUNT events.
- *
- *
- * Code was separated from {@link StorageReceiver} because we don't want that
- * receiver to be statically defined in manifest.
- *
- */
-public class StorageGoneReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(final Context context, final Intent intent) {
- final String action = intent.getAction();
- final Uri uri = intent.getData();
-
- if (uri == null || uri.getPath() == null) {
- return;
- }
-
- Timber.v("StorageGoneReceiver: %s", intent);
-
- final String path = uri.getPath();
-
- if (Intent.ACTION_MEDIA_EJECT.equals(action)) {
- StorageManager.getInstance(context).onBeforeUnmount(path);
- } else if (Intent.ACTION_MEDIA_UNMOUNTED.equals(action)) {
- StorageManager.getInstance(context).onAfterUnmount(path);
- }
- }
-
-}
diff --git a/app/core/src/main/java/com/fsck/k9/service/StorageReceiver.java b/app/core/src/main/java/com/fsck/k9/service/StorageReceiver.java
deleted file mode 100644
index a3a93e4715922bee4174aa30af537c63ac08de9a..0000000000000000000000000000000000000000
--- a/app/core/src/main/java/com/fsck/k9/service/StorageReceiver.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.fsck.k9.service;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import timber.log.Timber;
-
-import com.fsck.k9.mailstore.StorageManager;
-
-/**
- * That BroadcastReceiver is only interested in MOUNT events.
- */
-public class StorageReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(final Context context, final Intent intent) {
- final String action = intent.getAction();
- final Uri uri = intent.getData();
-
- if (uri == null || uri.getPath() == null) {
- return;
- }
-
- Timber.v("StorageReceiver: %s", intent);
-
- final String path = uri.getPath();
-
- if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
- StorageManager.getInstance(context).onMount(path,
- intent.getBooleanExtra("read-only", true));
- }
- }
-
-}
diff --git a/app/core/src/main/res/values/arrays_account_settings_values.xml b/app/core/src/main/res/values/arrays_account_settings_values.xml
index 7ed44826d393de940f3b25e8da2384908648de67..c293fd9168977f3779167ebc5346f91c202d875f 100644
--- a/app/core/src/main/res/values/arrays_account_settings_values.xml
+++ b/app/core/src/main/res/values/arrays_account_settings_values.xml
@@ -24,6 +24,42 @@
- 0xFF455A64
+
+
+
+ - 0xFFFFB300
+ - 0xFFFB8C00
+ - 0xFFF4511E
+ - 0xFFE53935
+
+ - 0xFFC0CA33
+ - 0xFF7CB342
+ - 0xFF388E3C
+ - 0xFF00897B
+
+ - 0xFF00ACC1
+ - 0xFF039BE5
+ - 0xFF1976D2
+ - 0xFF3949AB
+
+ - 0xFFE91E63
+ - 0xFF8E24AA
+ - 0xFF5E35B1
+ - 0xFF455A64
+
+
+ - 0xFFFF0000
+ - 0xFF00FF00
+ - 0xFF0000FF
+ - 0xFFFFFFFF
+
+ - 0xFFFFFF00
+ - 0xFF00FFFF
+ - 0xFFFF00FF
+ - 0x00000000
+
+
+
- -1
- 15
@@ -176,19 +212,6 @@
- 5
-
- - 1
- - 2
- - 3
- - 4
- - 5
- - 6
- - 7
- - 8
- - 9
- - 10
-
-
- PREFIX
- HEADER
diff --git a/app/core/src/main/res/values/arrays_general_settings_values.xml b/app/core/src/main/res/values/arrays_general_settings_values.xml
index bbfe6bd2f70ae30f6cd7ff65397b71288494fa90..560eb00e8edfac15ef728c39ebdaddb1844b1ec9 100644
--- a/app/core/src/main/res/values/arrays_general_settings_values.xml
+++ b/app/core/src/main/res/values/arrays_general_settings_values.xml
@@ -171,12 +171,6 @@
- 6
-
- - NEVER
- - WHEN_LOCKED
- - ALWAYS
-
-
- NEVER
- FOR_SINGLE_MSG
diff --git a/app/core/src/test/java/com/fsck/k9/QuietTimeCheckerTest.kt b/app/core/src/test/java/com/fsck/k9/QuietTimeCheckerTest.kt
index f1ca422f2d5e0bbe20c312d60fde199e09f2a860..19c67aced53a62d8751f454dc1f8fdf56f284978 100644
--- a/app/core/src/test/java/com/fsck/k9/QuietTimeCheckerTest.kt
+++ b/app/core/src/test/java/com/fsck/k9/QuietTimeCheckerTest.kt
@@ -4,11 +4,9 @@ import java.util.Calendar
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
-import org.mockito.Mockito.mock
-import org.mockito.kotlin.whenever
class QuietTimeCheckerTest {
- private val clock = mock(Clock::class.java)
+ private val clock = TestClock()
@Test
fun endTimeBeforeStartTime_timeIsBeforeEndOfQuietTime() {
@@ -113,7 +111,6 @@ class QuietTimeCheckerTest {
calendar.set(Calendar.HOUR_OF_DAY, hourOfDay)
calendar.set(Calendar.MINUTE, minute)
- val timeInMillis = calendar.timeInMillis
- whenever(clock.time).thenReturn(timeInMillis)
+ clock.time = calendar.timeInMillis
}
}
diff --git a/app/core/src/test/java/com/fsck/k9/TestApp.kt b/app/core/src/test/java/com/fsck/k9/TestApp.kt
index 21a36f3eeb480ab02c8f3dda14323dfb053a2bfe..b365bd4ddc5c2190f0740ad49585b8d54d791a4c 100644
--- a/app/core/src/test/java/com/fsck/k9/TestApp.kt
+++ b/app/core/src/test/java/com/fsck/k9/TestApp.kt
@@ -16,7 +16,7 @@ import org.mockito.kotlin.mock
class TestApp : Application() {
override fun onCreate() {
- Core.earlyInit(this)
+ Core.earlyInit()
super.onCreate()
DI.start(this, coreModules + storageModule + testModule)
diff --git a/app/core/src/test/java/com/fsck/k9/TestCoreResourceProvider.kt b/app/core/src/test/java/com/fsck/k9/TestCoreResourceProvider.kt
index d3052bf46448bca22653ae3badefe6f4b4650e0b..d13be3af8afb9a10c3613b3ba3b97f52f7f7c8a5 100644
--- a/app/core/src/test/java/com/fsck/k9/TestCoreResourceProvider.kt
+++ b/app/core/src/test/java/com/fsck/k9/TestCoreResourceProvider.kt
@@ -7,14 +7,6 @@ class TestCoreResourceProvider : CoreResourceProvider {
override fun defaultIdentityDescription() = "initial identity"
- override fun internalStorageProviderName(): String {
- throw UnsupportedOperationException("not implemented")
- }
-
- override fun externalStorageProviderName(): String {
- throw UnsupportedOperationException("not implemented")
- }
-
override fun contactDisplayNamePrefix() = "To:"
override fun contactUnknownSender() = ""
override fun contactUnknownRecipient() = ""
@@ -34,8 +26,6 @@ class TestCoreResourceProvider : CoreResourceProvider {
override fun replyHeader(sender: String) = "$sender wrote:"
override fun replyHeader(sender: String, sentDate: String) = "On $sentDate, $sender wrote:"
- override fun searchAllMessagesTitle() = "All messages"
- override fun searchAllMessagesDetail() = "All messages in searchable folders"
override fun searchUnifiedInboxTitle() = "Unified Inbox"
override fun searchUnifiedInboxDetail() = "All messages in unified folders"
diff --git a/app/core/src/test/java/com/fsck/k9/controller/MessageReferenceTest.java b/app/core/src/test/java/com/fsck/k9/controller/MessageReferenceTest.java
deleted file mode 100644
index 97f5166a2b4f152e749339c377eebc727c9d0cf2..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/controller/MessageReferenceTest.java
+++ /dev/null
@@ -1,209 +0,0 @@
-package com.fsck.k9.controller;
-
-
-import com.fsck.k9.mail.Flag;
-import org.junit.Test;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
-import static org.junit.Assert.assertNotNull;
-
-
-public class MessageReferenceTest {
-
- @Test
- public void checkIdentityStringFromMessageReferenceWithoutFlag() {
- MessageReference messageReference = createMessageReference("o hai!", 2, "10101010");
-
- assertEquals("#:byBoYWkh:Mg==:MTAxMDEwMTA=", messageReference.toIdentityString());
- }
-
- @Test
- public void checkIdentityStringFromMessageReferenceWithFlag() {
- MessageReference messageReference = createMessageReferenceWithFlag("o hai!", 2, "10101010", Flag.ANSWERED);
-
- assertEquals("#:byBoYWkh:Mg==:MTAxMDEwMTA=:ANSWERED", messageReference.toIdentityString());
- }
-
- @Test
- public void parseIdentityStringWithoutFlag() {
- MessageReference messageReference = MessageReference.parse("#:byBoYWkh:Mg==:MTAxMDEwMTA=");
-
- assertNotNull(messageReference);
- assertEquals("o hai!", messageReference.getAccountUuid());
- assertEquals(2, messageReference.getFolderId());
- assertEquals("10101010", messageReference.getUid());
- assertNull(messageReference.getFlag());
- }
-
- @Test
- public void parseIdentityStringWithFlag() {
- MessageReference messageReference = MessageReference.parse("#:byBoYWkh:Mg==:MTAxMDEwMTA=:ANSWERED");
-
- assertNotNull(messageReference);
- assertEquals("o hai!", messageReference.getAccountUuid());
- assertEquals(2, messageReference.getFolderId());
- assertEquals("10101010", messageReference.getUid());
- assertEquals(Flag.ANSWERED, messageReference.getFlag());
- }
-
- @Test
- public void checkMessageReferenceWithChangedUid() {
- MessageReference messageReferenceOne = createMessageReferenceWithFlag("account", 1, "uid", Flag.SEEN);
-
- MessageReference messageReferenceTwo = messageReferenceOne.withModifiedUid("---");
-
- assertEquals("account", messageReferenceTwo.getAccountUuid());
- assertEquals(1, messageReferenceTwo.getFolderId());
- assertEquals("---", messageReferenceTwo.getUid());
- assertEquals(Flag.SEEN, messageReferenceTwo.getFlag());
- }
-
- @Test
- public void checkMessageReferenceWithChangedFlag() {
- MessageReference messageReferenceOne = createMessageReferenceWithFlag("account", 1, "uid", Flag.ANSWERED);
-
- MessageReference messageReferenceTwo = messageReferenceOne.withModifiedFlag(Flag.DELETED);
-
- assertEquals("account", messageReferenceTwo.getAccountUuid());
- assertEquals(1, messageReferenceTwo.getFolderId());
- assertEquals("uid", messageReferenceTwo.getUid());
- assertEquals(Flag.DELETED, messageReferenceTwo.getFlag());
- }
-
- @Test
- public void parseIdentityStringContainingBadVersionNumber() {
- MessageReference messageReference = MessageReference.parse("@:byBoYWkh:MTAxMDEwMTA=:ANSWERED");
-
- assertNull(messageReference);
- }
-
- @SuppressWarnings("ConstantConditions")
- @Test
- public void parseNullIdentityString() {
- MessageReference messageReference = MessageReference.parse(null);
-
- assertNull(messageReference);
- }
-
- @Test
- public void parseIdentityStringWithCorruptFlag() {
- MessageReference messageReference =
- MessageReference.parse("!:%^&%^*$&$by&(BYWkh:Zm9%^@sZGVy:MT-35#$AxMDEwMTA=:ANSWE!RED");
-
- assertNull(messageReference);
- }
-
- @Test
- public void equalsWithAnObjectShouldReturnFalse() {
- MessageReference messageReference = new MessageReference("a", 1, "c", null);
- Object object = new Object();
-
- assertFalse(messageReference.equals(object));
- }
-
- @SuppressWarnings({"ObjectEqualsNull", "ConstantConditions"})
- @Test
- public void equalsWithNullShouldReturnFalse() {
- MessageReference messageReference = createMessageReference("account", 1, "uid");
-
- assertFalse(messageReference.equals(null));
- }
-
- @SuppressWarnings("EqualsWithItself")
- @Test
- public void equalsWithSameMessageReferenceShouldReturnTrue() {
- MessageReference messageReference = createMessageReference("account", 1, "uid");
-
- assertTrue(messageReference.equals(messageReference));
- }
-
- @Test
- public void equalsWithMessageReferenceContainingSameDataShouldReturnTrue() {
- MessageReference messageReferenceOne = createMessageReference("account", 1, "uid");
- MessageReference messageReferenceTwo = createMessageReference("account", 1, "uid");
-
- assertEqualsReturnsTrueSymmetrically(messageReferenceOne, messageReferenceTwo);
- }
-
- @Test
- public void equalsWithMessageReferenceContainingDifferentAccountUuidShouldReturnFalse() {
- MessageReference messageReferenceOne = createMessageReference("account", 1, "uid");
- MessageReference messageReferenceTwo = createMessageReference("-------", 1, "uid");
-
- assertEqualsReturnsFalseSymmetrically(messageReferenceOne, messageReferenceTwo);
- }
-
- @Test
- public void equalsWithMessageReferenceContainingDifferentFolderNameShouldReturnFalse() {
- MessageReference messageReferenceOne = createMessageReference("account", 1, "uid");
- MessageReference messageReferenceTwo = createMessageReference("account", 8, "uid");
-
- assertEqualsReturnsFalseSymmetrically(messageReferenceOne, messageReferenceTwo);
- }
-
- @Test
- public void equalsWithMessageReferenceContainingDifferentUidShouldReturnFalse() {
- MessageReference messageReferenceOne = createMessageReference("account", 1, "uid");
- MessageReference messageReferenceTwo = createMessageReference("account", 1, "---");
-
- assertEqualsReturnsFalseSymmetrically(messageReferenceOne, messageReferenceTwo);
- }
-
- @Test
- public void alternativeEquals() {
- MessageReference messageReference = createMessageReference("account", 1, "uid");
-
- boolean equalsResult = messageReference.equals("account", 1, "uid");
-
- assertTrue(equalsResult);
- }
-
- @Test
- public void equals_withNullAccount_shouldReturnFalse() {
- MessageReference messageReference = createMessageReference("account", 1, "uid");
-
- boolean equalsResult = messageReference.equals(null, 1, "uid");
-
- assertFalse(equalsResult);
- }
-
- @Test
- public void equals_withNullUid_shouldReturnFalse() {
- MessageReference messageReference = createMessageReference("account", 1, "uid");
-
- boolean equalsResult = messageReference.equals("account", 1, null);
-
- assertFalse(equalsResult);
- }
-
- @Test(expected = NullPointerException.class)
- public void constructor_withNullAccount_shouldThrow() {
- createMessageReference(null, 1, "uid");
- }
-
- @Test(expected = NullPointerException.class)
- public void constructor_withNullUid_shouldThrow() {
- createMessageReference("account", 1, null);
- }
-
- private MessageReference createMessageReference(String accountUuid, long folderId, String uid) {
- return new MessageReference(accountUuid, folderId, uid, null);
- }
-
- private MessageReference createMessageReferenceWithFlag(String accountUuid, long folderId, String uid, Flag flag) {
- return new MessageReference(accountUuid, folderId, uid, flag);
- }
-
- private void assertEqualsReturnsTrueSymmetrically(MessageReference referenceOne, MessageReference referenceTwo) {
- assertTrue(referenceOne.equals(referenceTwo));
- assertTrue(referenceTwo.equals(referenceOne));
- }
-
- private void assertEqualsReturnsFalseSymmetrically(MessageReference referenceOne, MessageReference referenceTwo) {
- assertFalse(referenceOne.equals(referenceTwo));
- assertFalse(referenceTwo.equals(referenceOne));
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/controller/MessageReferenceTest.kt b/app/core/src/test/java/com/fsck/k9/controller/MessageReferenceTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..495f053c19eccce707cc5e82bb5e1ef919bc8561
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/controller/MessageReferenceTest.kt
@@ -0,0 +1,61 @@
+package com.fsck.k9.controller
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
+import org.junit.Test
+
+class MessageReferenceTest {
+ @Test
+ fun checkIdentityStringFromMessageReference() {
+ val messageReference = MessageReference("o hai!", 2, "10101010")
+
+ val serialized = messageReference.toIdentityString()
+
+ assertThat(serialized).isEqualTo("#:byBoYWkh:Mg==:MTAxMDEwMTA=")
+ }
+
+ @Test
+ fun parseIdentityString() {
+ val result = MessageReference.parse("#:byBoYWkh:Mg==:MTAxMDEwMTA=")
+
+ assertNotNull(result) { messageReference ->
+ assertThat(messageReference.accountUuid).isEqualTo("o hai!")
+ assertThat(messageReference.folderId).isEqualTo(2)
+ assertThat(messageReference.uid).isEqualTo("10101010")
+ }
+ }
+
+ @Test
+ fun parseIdentityStringContainingBadVersionNumber() {
+ val messageReference = MessageReference.parse("@:byBoYWkh:MTAxMDEwMTA=")
+
+ assertThat(messageReference).isNull()
+ }
+
+ @Test
+ fun parseNullIdentityString() {
+ val messageReference = MessageReference.parse(null)
+
+ assertThat(messageReference).isNull()
+ }
+
+ @Test
+ fun checkMessageReferenceWithChangedUid() {
+ val messageReferenceOne = MessageReference("account", 1, "uid")
+
+ val messageReference = messageReferenceOne.withModifiedUid("---")
+
+ assertThat(messageReference.accountUuid).isEqualTo("account")
+ assertThat(messageReference.folderId).isEqualTo(1)
+ assertThat(messageReference.uid).isEqualTo("---")
+ }
+
+ @Test
+ fun alternativeEquals() {
+ val messageReference = MessageReference("account", 1, "uid")
+
+ val equalsResult = messageReference.equals("account", 1, "uid")
+
+ assertThat(equalsResult).isTrue()
+ }
+}
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 c7e6429b527c564c5e127063bbc9929baf0d8e1f..6525e7423d759ec5bd47e6a493175dee14be2797 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
@@ -14,11 +14,14 @@ import com.fsck.k9.K9RobolectricTest;
import com.fsck.k9.Preferences;
import com.fsck.k9.backend.BackendManager;
import com.fsck.k9.backend.api.Backend;
+import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException;
+import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.LocalStore;
@@ -28,9 +31,9 @@ import com.fsck.k9.mailstore.OutboxState;
import com.fsck.k9.mailstore.OutboxStateRepository;
import com.fsck.k9.mailstore.SaveMessageDataCreator;
import com.fsck.k9.mailstore.SendState;
-import com.fsck.k9.mailstore.UnavailableStorageException;
import com.fsck.k9.notification.NotificationController;
import com.fsck.k9.notification.NotificationStrategy;
+import com.fsck.k9.preferences.Protocols;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchAccount;
import org.jetbrains.annotations.NotNull;
@@ -50,14 +53,14 @@ import org.robolectric.shadows.ShadowLog;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -110,15 +113,15 @@ public class MessagingControllerTest extends K9RobolectricTest {
private LocalMessage localMessageToSend1;
private volatile boolean hasFetchedMessage = false;
- private UnreadMessageCountProvider unreadMessageCountProvider = new UnreadMessageCountProvider() {
+ private MessageCountsProvider messageCountsProvider = new MessageCountsProvider() {
@Override
- public int getUnreadMessageCount(@NotNull SearchAccount searchAccount) {
- return 0;
+ public MessageCounts getMessageCounts(@NotNull SearchAccount searchAccount) {
+ return new MessageCounts(0, 0);
}
@Override
- public int getUnreadMessageCount(@NotNull Account account) {
- return 0;
+ public MessageCounts getMessageCounts(@NotNull Account account) {
+ return new MessageCounts(0, 0);
}
};
@@ -135,7 +138,7 @@ public class MessagingControllerTest extends K9RobolectricTest {
preferences = Preferences.getPreferences(appContext);
controller = new MessagingController(appContext, notificationController, notificationStrategy,
- localStoreProvider, unreadMessageCountProvider, backendManager, preferences, messageStoreManager,
+ localStoreProvider, messageCountsProvider, backendManager, preferences, messageStoreManager,
saveMessageDataCreator, Collections.emptyList());
configureAccount();
@@ -164,13 +167,6 @@ public class MessagingControllerTest extends K9RobolectricTest {
verify(localFolder).clearAllMessages();
}
- @Test(expected = UnavailableAccountException.class)
- public void clearFolderSynchronous_whenStorageUnavailable_shouldThrowUnavailableAccountException() throws MessagingException {
- doThrow(new UnavailableStorageException("Test")).when(localFolder).open();
-
- controller.clearFolderSynchronous(account, FOLDER_ID);
- }
-
@Test
public void refreshRemoteSynchronous_shouldCallBackend() throws MessagingException {
controller.refreshFolderListSynchronous(account);
@@ -334,7 +330,7 @@ public class MessagingControllerTest extends K9RobolectricTest {
controller.sendPendingMessagesSynchronous(account);
- verifyZeroInteractions(listener);
+ verifyNoMoreInteractions(listener);
}
@Test
@@ -441,6 +437,10 @@ public class MessagingControllerTest extends K9RobolectricTest {
account = preferences.newAccount();
accountUuid = account.getUuid();
+ account.setIncomingServerSettings(new ServerSettings(Protocols.IMAP, "host", 993,
+ ConnectionSecurity.SSL_TLS_REQUIRED, AuthType.PLAIN, "username", "password", null));
+ account.setOutgoingServerSettings(new ServerSettings(Protocols.SMTP, "host", 465,
+ ConnectionSecurity.SSL_TLS_REQUIRED, AuthType.PLAIN, "username", "password", null));
account.setMaximumAutoDownloadMessageSize(MAXIMUM_SMALL_MESSAGE_SIZE);
account.setEmail("user@host.com");
}
diff --git a/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.java b/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.java
index f5b7cc0eb7c1920a64d18c2a68773340fa81f804..85347a322004908c4d3df12254c5f9572aa6c7d1 100644
--- a/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.java
+++ b/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.java
@@ -94,6 +94,20 @@ public class MessageHelperTest extends RobolectricTest {
assertEquals("test@testor.com", friendly.toString());
}
+ @Test
+ public void toFriendly_atPrecededByOpeningParenthesisShouldNotTriggerSpoofPrevention() {
+ Address address = new Address("gitlab@gitlab.example", "username (@username)");
+ CharSequence friendly = MessageHelper.toFriendly(address, contacts);
+ assertEquals("username (@username)", friendly.toString());
+ }
+
+ @Test
+ public void toFriendly_nameStartingWithAtShouldNotTriggerSpoofPrevention() {
+ Address address = new Address("address@domain.example", "@username");
+ CharSequence friendly = MessageHelper.toFriendly(address, contacts);
+ assertEquals("@username", friendly.toString());
+ }
+
@Test
public void toFriendly_spoofPreventionDoesntOverrideContact() {
Address address = new Address("test@testor.com", "Tim Testor");
diff --git a/app/core/src/test/java/com/fsck/k9/helper/ReplyToParserTest.java b/app/core/src/test/java/com/fsck/k9/helper/ReplyToParserTest.java
index d612a7413ab2f3c60db27a8e5e2505aaa75b565c..501b2731b965eb759a871ff1604915109a80719d 100644
--- a/app/core/src/test/java/com/fsck/k9/helper/ReplyToParserTest.java
+++ b/app/core/src/test/java/com/fsck/k9/helper/ReplyToParserTest.java
@@ -17,8 +17,8 @@ import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
diff --git a/app/core/src/test/java/com/fsck/k9/logging/LogcatLogFileWriterTest.kt b/app/core/src/test/java/com/fsck/k9/logging/LogcatLogFileWriterTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2fcc6e93ff9ebc590d804dd821a2a79ef2a1b473
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/logging/LogcatLogFileWriterTest.kt
@@ -0,0 +1,85 @@
+package com.fsck.k9.logging
+
+import android.content.ContentResolver
+import android.net.Uri
+import com.google.common.truth.Truth.assertThat
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.io.InputStream
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+class LogcatLogFileWriterTest {
+ private val contentUri = mock()
+ private val outputStream = ByteArrayOutputStream()
+
+ @Test
+ fun `write log to contentUri`() = runBlocking {
+ val logData = "a".repeat(10_000)
+ val logFileWriter = LogcatLogFileWriter(
+ contentResolver = createContentResolver(),
+ processExecutor = createProcessExecutor(logData),
+ coroutineDispatcher = Dispatchers.Unconfined
+ )
+
+ logFileWriter.writeLogTo(contentUri)
+
+ assertThat(outputStream.toByteArray().decodeToString()).isEqualTo(logData)
+ }
+
+ @Test(expected = FileNotFoundException::class)
+ fun `contentResolver throws`() = runBlocking {
+ val logFileWriter = LogcatLogFileWriter(
+ contentResolver = createThrowingContentResolver(FileNotFoundException()),
+ processExecutor = createProcessExecutor("irrelevant"),
+ coroutineDispatcher = Dispatchers.Unconfined
+ )
+
+ logFileWriter.writeLogTo(contentUri)
+ }
+
+ @Test(expected = IOException::class)
+ fun `processExecutor throws`() = runBlocking {
+ val logFileWriter = LogcatLogFileWriter(
+ contentResolver = createContentResolver(),
+ processExecutor = ThrowingProcessExecutor(IOException()),
+ coroutineDispatcher = Dispatchers.Unconfined
+ )
+
+ logFileWriter.writeLogTo(contentUri)
+ }
+
+ private fun createContentResolver(): ContentResolver {
+ return mock {
+ on { openOutputStream(contentUri) } doReturn outputStream
+ }
+ }
+
+ private fun createThrowingContentResolver(exception: Exception): ContentResolver {
+ return mock {
+ on { openOutputStream(contentUri) } doAnswer { throw exception }
+ }
+ }
+
+ private fun createProcessExecutor(logData: String): DataProcessExecutor {
+ return DataProcessExecutor(logData.toByteArray(charset = Charsets.US_ASCII))
+ }
+}
+
+private class DataProcessExecutor(val data: ByteArray) : ProcessExecutor {
+ override fun exec(command: String): InputStream {
+ return ByteArrayInputStream(data)
+ }
+}
+
+private class ThrowingProcessExecutor(val exception: Exception) : ProcessExecutor {
+ override fun exec(command: String): InputStream {
+ throw exception
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendFolderTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendFolderTest.kt
index 197608f3f9146c2687b062fff645b8959eb8abfd..49bc6fc81dc1d75be1da3bca4e96f80fbcf6bb21 100644
--- a/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendFolderTest.kt
+++ b/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendFolderTest.kt
@@ -78,16 +78,6 @@ class K9BackendFolderTest : K9RobolectricTest() {
assertEquals(flags, messageFlags)
}
- @Test
- fun getLastUid() {
- createMessageInBackendFolder("200")
- createMessageInBackendFolder("123")
-
- val lastUid = backendFolder.getLastUid()
-
- assertEquals(200L, lastUid)
- }
-
@Test
fun saveCompleteMessage_withoutServerId_shouldThrow() {
val message = createMessage(messageServerId = null)
diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java b/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java
index 6c374556af13df61736e373fa7e00ebb595b023f..563286c8e552601eee6ffb27e46cb7c93c6f08cd 100644
--- a/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java
+++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java
@@ -2,6 +2,7 @@ package com.fsck.k9.mailstore;
import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -17,6 +18,7 @@ import com.fsck.k9.DI;
import com.fsck.k9.K9RobolectricTest;
import com.fsck.k9.TestCoreResourceProvider;
import com.fsck.k9.mail.Address;
+import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
@@ -48,7 +50,7 @@ import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertSame;
import static junit.framework.Assert.assertTrue;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -135,7 +137,7 @@ public class MessageViewInfoExtractorTest extends K9RobolectricTest {
@Test
public void testTextPlainFormatFlowed() throws MessagingException {
// Create text/plain body
- TextBody body = new TextBody(BODY_TEXT_FLOWED);
+ Body body = new BinaryMemoryBody(BODY_TEXT_FLOWED.getBytes(StandardCharsets.UTF_8), "utf-8");
// Create message
MimeMessage message = new MimeMessage();
diff --git a/app/core/src/test/java/com/fsck/k9/message/IdentityHeaderBuilderTest.kt b/app/core/src/test/java/com/fsck/k9/message/IdentityHeaderBuilderTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2be6075663d483c2f0daaf0a17703bfd3f99f70f
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/message/IdentityHeaderBuilderTest.kt
@@ -0,0 +1,56 @@
+package com.fsck.k9.message
+
+import com.fsck.k9.Account.QuoteStyle
+import com.fsck.k9.Identity
+import com.fsck.k9.RobolectricTest
+import com.fsck.k9.mail.internet.MimeHeaderChecker
+import com.fsck.k9.mail.internet.TextBody
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+private const val IDENTITY_HEADER = "X-K9mail-Identity"
+
+class IdentityHeaderBuilderTest : RobolectricTest() {
+ @Test
+ fun `valid unstructured header field value`() {
+ val signature = "a".repeat(1000)
+
+ val identityHeader = IdentityHeaderBuilder()
+ .setCursorPosition(0)
+ .setIdentity(createIdentity(signatureUse = true))
+ .setIdentityChanged(false)
+ .setMessageFormat(SimpleMessageFormat.TEXT)
+ .setMessageReference(null)
+ .setQuotedHtmlContent(null)
+ .setQuoteStyle(QuoteStyle.PREFIX)
+ .setQuoteTextMode(QuotedTextMode.NONE)
+ .setSignature(signature)
+ .setSignatureChanged(true)
+ .setBody(TextBody("irrelevant"))
+ .setBodyPlain(null)
+ .build()
+
+ assertThat(identityHeader.length).isGreaterThan(1000)
+ assertIsValidHeader(identityHeader)
+ }
+
+ private fun assertIsValidHeader(identityHeader: String) {
+ try {
+ MimeHeaderChecker.checkHeader(IDENTITY_HEADER, identityHeader)
+ } catch (e: Exception) {
+ println("$IDENTITY_HEADER: $identityHeader")
+ throw e
+ }
+ }
+
+ private fun createIdentity(
+ description: String? = null,
+ name: String? = null,
+ email: String? = null,
+ signature: String? = null,
+ signatureUse: Boolean = false,
+ replyTo: String? = null
+ ): Identity {
+ return Identity(description, name, email, signature, signatureUse, replyTo)
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/message/IdentityHeaderParserTest.kt b/app/core/src/test/java/com/fsck/k9/message/IdentityHeaderParserTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1d58f7c2125ec1d20b398c2f4cc614847b37f0b5
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/message/IdentityHeaderParserTest.kt
@@ -0,0 +1,33 @@
+package com.fsck.k9.message
+
+import com.fsck.k9.RobolectricTest
+import com.fsck.k9.helper.toCrLf
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class IdentityHeaderParserTest : RobolectricTest() {
+ @Test
+ fun `folded header value`() {
+ val input = """
+ |!l=10&o=0&qs=PREFIX&f=TEXT&s=aaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&p=0&q=NONE
+ """.trimMargin().toCrLf()
+
+ val result = IdentityHeaderParser.parse(input)
+
+ assertThat(result).containsEntry(IdentityField.SIGNATURE, "a".repeat(1000))
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/message/MessageBuilderTest.java b/app/core/src/test/java/com/fsck/k9/message/MessageBuilderTest.java
index 0c745e9402cf5a7c1bfa95a480c1cc5ef1865b1e..267e448b838175f89a82242ecbc3b221fb5b901d 100644
--- a/app/core/src/test/java/com/fsck/k9/message/MessageBuilderTest.java
+++ b/app/core/src/test/java/com/fsck/k9/message/MessageBuilderTest.java
@@ -38,8 +38,8 @@ import org.robolectric.annotation.LooperMode;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -52,6 +52,10 @@ public class MessageBuilderTest extends RobolectricTest {
private static final String TEST_ATTACHMENT_TEXT = "text data in attachment";
private static final String TEST_SUBJECT = "test_subject";
private static final Address TEST_IDENTITY_ADDRESS = new Address("test@example.org", "tester");
+ private static final Address[] TEST_REPLY_TO = new Address[] {
+ new Address("reply-to1@example.org", "reply 1"),
+ new Address("reply-to2@example.org", "reply 2")
+ };
private static final Address[] TEST_TO = new Address[] {
new Address("to1@example.org", "recip 1"),
new Address("to2@example.org", "recip 2")
@@ -75,6 +79,7 @@ public class MessageBuilderTest extends RobolectricTest {
"BCC: bcc recip \r\n" +
"Subject: test_subject\r\n" +
"User-Agent: K-9 Mail for Android\r\n" +
+ "Reply-to: reply 1 , reply 2 \r\n" +
"In-Reply-To: inreplyto\r\n" +
"References: references\r\n" +
"Message-ID: " + TEST_MESSAGE_ID + "\r\n" +
@@ -214,6 +219,7 @@ public class MessageBuilderTest extends RobolectricTest {
assertEquals("text/plain", message.getMimeType());
assertEquals(TEST_SUBJECT, message.getSubject());
assertEquals(TEST_IDENTITY_ADDRESS, message.getFrom()[0]);
+ assertArrayEquals(TEST_REPLY_TO, message.getReplyTo());
assertArrayEquals(TEST_TO, message.getRecipients(RecipientType.TO));
assertArrayEquals(TEST_CC, message.getRecipients(RecipientType.CC));
assertArrayEquals(TEST_BCC, message.getRecipients(RecipientType.BCC));
@@ -432,6 +438,7 @@ public class MessageBuilderTest extends RobolectricTest {
.setSubject(TEST_SUBJECT)
.setSentDate(SENT_DATE)
.setHideTimeZone(true)
+ .setReplyTo(TEST_REPLY_TO)
.setTo(Arrays.asList(TEST_TO))
.setCc(Arrays.asList(TEST_CC))
.setBcc(Arrays.asList(TEST_BCC))
diff --git a/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.java b/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.java
index 8dfbbaacdea71b8e48566e33fa78509b5f2d7d18..d7c34f9fb7308985a15fe6559d430baaaebda237 100644
--- a/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.java
+++ b/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.java
@@ -306,4 +306,24 @@ public class HtmlConverterTest {
assertEquals("https://domain.example/path/", result);
}
+
+ @Test
+ public void htmlToText_withLineBreaksInHtml() {
+ String input = "One\nTwo\r\nThree";
+
+ String result = HtmlConverter.htmlToText(input);
+
+ assertEquals("One Two Three", result);
+ }
+
+ @Test
+ public void htmlToText_withLongTextLine_shouldNotAddLineBreaksToOutput() {
+ String input = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam sit amet finibus felis, " +
+ "viverra ullamcorper justo. Suspendisse potenti. Etiam erat sem, interdum a condimentum quis, " +
+ "fringilla quis orci.";
+
+ String result = HtmlConverter.htmlToText(input);
+
+ assertEquals(input, result);
+ }
}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/AddNotificationResultTest.java b/app/core/src/test/java/com/fsck/k9/notification/AddNotificationResultTest.java
deleted file mode 100644
index 3dbc35b5594a5c4b9faf80cb08191cf185edc319..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/AddNotificationResultTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-
-public class AddNotificationResultTest {
- private static final int NOTIFICATION_ID = 23;
-
-
- private NotificationHolder notificationHolder;
-
-
- @Before
- public void setUp() throws Exception {
- notificationHolder = new NotificationHolder(NOTIFICATION_ID, null);
- }
-
- @Test
- public void newNotification_shouldCancelNotification_shouldReturnFalse() throws Exception {
- AddNotificationResult result = AddNotificationResult.newNotification(notificationHolder);
-
- assertFalse(result.shouldCancelNotification());
- }
-
- @Test(expected = IllegalStateException.class)
- public void newNotification_getNotificationId_shouldReturnNotificationId() throws Exception {
- AddNotificationResult result = AddNotificationResult.newNotification(notificationHolder);
-
- result.getNotificationId();
- }
-
- @Test
- public void replaceNotification_shouldCancelNotification_shouldReturnTrue() throws Exception {
- AddNotificationResult result = AddNotificationResult.replaceNotification(notificationHolder);
-
- assertTrue(result.shouldCancelNotification());
- }
-
- @Test
- public void replaceNotification_getNotificationId_shouldReturnNotificationId() throws Exception {
- AddNotificationResult result = AddNotificationResult.replaceNotification(notificationHolder);
-
- assertEquals(NOTIFICATION_ID, result.getNotificationId());
- }
-
- @Test
- public void getNotificationHolder_shouldReturnNotificationHolder() throws Exception {
- AddNotificationResult result = AddNotificationResult.replaceNotification(notificationHolder);
-
- assertEquals(notificationHolder, result.getNotificationHolder());
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/AuthenticationErrorNotificationControllerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/AuthenticationErrorNotificationControllerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..af796ff184e9a011110b80a515c898c94e920a2f
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/AuthenticationErrorNotificationControllerTest.kt
@@ -0,0 +1,121 @@
+package com.fsck.k9.notification
+
+import android.app.Notification
+import android.app.PendingIntent
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.core.app.ApplicationProvider
+import com.fsck.k9.Account
+import com.fsck.k9.RobolectricTest
+import com.fsck.k9.testing.MockHelper.mockBuilder
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+
+private const val INCOMING = true
+private const val OUTGOING = false
+private const val ACCOUNT_NUMBER = 1
+private const val ACCOUNT_NAME = "TestAccount"
+
+class AuthenticationErrorNotificationControllerTest : RobolectricTest() {
+ private val resourceProvider = TestNotificationResourceProvider()
+ private val notification = mock()
+ private val lockScreenNotification = mock()
+ private val notificationManager = mock()
+ private val builder = createFakeNotificationBuilder(notification)
+ private val lockScreenNotificationBuilder = createFakeNotificationBuilder(lockScreenNotification)
+ private val notificationHelper = createFakeNotificationHelper(
+ notificationManager,
+ builder,
+ lockScreenNotificationBuilder
+ )
+ private val account = createFakeAccount()
+ private val controller = TestAuthenticationErrorNotificationController()
+ private val contentIntent = mock()
+
+ @Test
+ fun showAuthenticationErrorNotification_withIncomingServer_shouldCreateNotification() {
+ val notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, INCOMING)
+
+ controller.showAuthenticationErrorNotification(account, INCOMING)
+
+ verify(notificationManager).notify(notificationId, notification)
+ assertAuthenticationErrorNotificationContents()
+ }
+
+ @Test
+ fun clearAuthenticationErrorNotification_withIncomingServer_shouldCancelNotification() {
+ val notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, INCOMING)
+
+ controller.clearAuthenticationErrorNotification(account, INCOMING)
+
+ verify(notificationManager).cancel(notificationId)
+ }
+
+ @Test
+ fun showAuthenticationErrorNotification_withOutgoingServer_shouldCreateNotification() {
+ val notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, OUTGOING)
+
+ controller.showAuthenticationErrorNotification(account, OUTGOING)
+
+ verify(notificationManager).notify(notificationId, notification)
+ assertAuthenticationErrorNotificationContents()
+ }
+
+ @Test
+ fun clearAuthenticationErrorNotification_withOutgoingServer_shouldCancelNotification() {
+ val notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, OUTGOING)
+
+ controller.clearAuthenticationErrorNotification(account, OUTGOING)
+
+ verify(notificationManager).cancel(notificationId)
+ }
+
+ private fun assertAuthenticationErrorNotificationContents() {
+ verify(builder).setSmallIcon(resourceProvider.iconWarning)
+ verify(builder).setTicker("Authentication failed")
+ verify(builder).setContentTitle("Authentication failed")
+ verify(builder).setContentText("Authentication failed for $ACCOUNT_NAME. Update your server settings.")
+ verify(builder).setContentIntent(contentIntent)
+ verify(builder).setPublicVersion(lockScreenNotification)
+ verify(lockScreenNotificationBuilder).setContentTitle("Authentication failed")
+ verify(lockScreenNotificationBuilder, never()).setContentText(any())
+ verify(lockScreenNotificationBuilder, never()).setTicker(any())
+ }
+
+ private fun createFakeNotificationBuilder(notification: Notification): NotificationCompat.Builder {
+ return mockBuilder {
+ on { build() } doReturn notification
+ }
+ }
+
+ private fun createFakeNotificationHelper(
+ notificationManager: NotificationManagerCompat,
+ notificationBuilder: NotificationCompat.Builder,
+ lockScreenNotificationBuilder: NotificationCompat.Builder
+ ): NotificationHelper {
+ return mock {
+ on { getContext() } doReturn ApplicationProvider.getApplicationContext()
+ on { getNotificationManager() } doReturn notificationManager
+ on { createNotificationBuilder(any(), any()) }.doReturn(notificationBuilder, lockScreenNotificationBuilder)
+ }
+ }
+
+ private fun createFakeAccount(): Account {
+ return mock {
+ on { accountNumber } doReturn ACCOUNT_NUMBER
+ on { description } doReturn ACCOUNT_NAME
+ }
+ }
+
+ internal inner class TestAuthenticationErrorNotificationController :
+ AuthenticationErrorNotificationController(notificationHelper, mock(), resourceProvider) {
+
+ override fun createContentIntent(account: Account, incoming: Boolean): PendingIntent {
+ return contentIntent
+ }
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/AuthenticationErrorNotificationsTest.java b/app/core/src/test/java/com/fsck/k9/notification/AuthenticationErrorNotificationsTest.java
deleted file mode 100644
index 558b6983f50cb9f613836699d9169d2b0d85c807..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/AuthenticationErrorNotificationsTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.Builder;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.testing.MockHelper;
-import com.fsck.k9.RobolectricTest;
-import org.junit.Before;
-import org.junit.Test;
-import org.robolectric.RuntimeEnvironment;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-
-public class AuthenticationErrorNotificationsTest extends RobolectricTest {
- private static final boolean INCOMING = true;
- private static final boolean OUTGOING = false;
- private static final int ACCOUNT_NUMBER = 1;
- private static final String ACCOUNT_NAME = "TestAccount";
-
-
- private NotificationResourceProvider resourceProvider = new TestNotificationResourceProvider();
- private Notification notification;
- private NotificationManagerCompat notificationManager;
- private NotificationCompat.Builder builder;
- private NotificationHelper notificationHelper;
- private Account account;
- private AuthenticationErrorNotifications authenticationErrorNotifications;
- private PendingIntent contentIntent;
-
-
- @Before
- public void setUp() throws Exception {
- notification = createFakeNotification();
- notificationManager = createFakeNotificationManager();
- builder = createFakeNotificationBuilder(notification);
- notificationHelper = createFakeNotificationHelper(notificationManager, builder);
- account = createFakeAccount();
- contentIntent = createFakeContentIntent();
-
- authenticationErrorNotifications = new TestAuthenticationErrorNotifications();
- }
-
- @Test
- public void showAuthenticationErrorNotification_withIncomingServer_shouldCreateNotification() throws Exception {
- int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, INCOMING);
-
- authenticationErrorNotifications.showAuthenticationErrorNotification(account, INCOMING);
-
- verify(notificationManager).notify(notificationId, notification);
- assertAuthenticationErrorNotificationContents();
- }
-
- @Test
- public void clearAuthenticationErrorNotification_withIncomingServer_shouldCancelNotification() throws Exception {
- int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, INCOMING);
-
- authenticationErrorNotifications.clearAuthenticationErrorNotification(account, INCOMING);
-
- verify(notificationManager).cancel(notificationId);
- }
-
- @Test
- public void showAuthenticationErrorNotification_withOutgoingServer_shouldCreateNotification() throws Exception {
- int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, OUTGOING);
-
- authenticationErrorNotifications.showAuthenticationErrorNotification(account, OUTGOING);
-
- verify(notificationManager).notify(notificationId, notification);
- assertAuthenticationErrorNotificationContents();
- }
-
- @Test
- public void clearAuthenticationErrorNotification_withOutgoingServer_shouldCancelNotification() throws Exception {
- int notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, OUTGOING);
-
- authenticationErrorNotifications.clearAuthenticationErrorNotification(account, OUTGOING);
-
- verify(notificationManager).cancel(notificationId);
- }
-
- private void assertAuthenticationErrorNotificationContents() {
- verify(builder).setSmallIcon(resourceProvider.getIconWarning());
- verify(builder).setTicker("Authentication failed");
- verify(builder).setContentTitle("Authentication failed");
- verify(builder).setContentText("Authentication failed for " + ACCOUNT_NAME + ". Update your server settings.");
- verify(builder).setContentIntent(contentIntent);
- verify(builder).setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
- }
-
- private Notification createFakeNotification() {
- return mock(Notification.class);
- }
-
- private NotificationManagerCompat createFakeNotificationManager() {
- return mock(NotificationManagerCompat.class);
- }
-
- private Builder createFakeNotificationBuilder(Notification notification) {
- Builder builder = MockHelper.mockBuilder(Builder.class);
- when(builder.build()).thenReturn(notification);
- return builder;
- }
-
- private NotificationHelper createFakeNotificationHelper(NotificationManagerCompat notificationManager,
- NotificationCompat.Builder builder) {
- NotificationHelper notificationHelper = mock(NotificationHelper.class);
- when(notificationHelper.getContext()).thenReturn(RuntimeEnvironment.application);
- when(notificationHelper.getNotificationManager()).thenReturn(notificationManager);
- when(notificationHelper.createNotificationBuilder(any(Account.class),
- any(NotificationChannelManager.ChannelType.class)))
- .thenReturn(builder);
-
- return notificationHelper;
- }
-
- private Account createFakeAccount() {
- Account account = mock(Account.class);
- when(account.getAccountNumber()).thenReturn(ACCOUNT_NUMBER);
- when(account.getDescription()).thenReturn(ACCOUNT_NAME);
-
- return account;
- }
-
- private PendingIntent createFakeContentIntent() {
- return mock(PendingIntent.class);
- }
-
-
- class TestAuthenticationErrorNotifications extends AuthenticationErrorNotifications {
- public TestAuthenticationErrorNotifications() {
- super(notificationHelper, mock(NotificationActionCreator.class), resourceProvider);
- }
-
- @Override
- PendingIntent createContentIntent(Account account, boolean incoming) {
- return contentIntent;
- }
- }
-}
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
new file mode 100644
index 0000000000000000000000000000000000000000..5b0cf007a33b8f46d9b08b749aff18f4bef83565
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationDataCreatorTest.kt
@@ -0,0 +1,198 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.Identity
+import com.fsck.k9.K9
+import com.fsck.k9.K9.LockScreenNotificationVisibility
+import com.fsck.k9.NotificationSetting
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+
+class BaseNotificationDataCreatorTest {
+ private val account = createAccount()
+ private val notificationDataCreator = BaseNotificationDataCreator()
+
+ @Test
+ fun `account instance`() {
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.account).isSameInstanceAs(account)
+ }
+
+ @Test
+ fun `account name from description property`() {
+ account.description = "description"
+ account.email = "irrelevant@k9mail.example"
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.accountName).isEqualTo("description")
+ }
+
+ @Test
+ fun `account description is blank`() {
+ account.description = ""
+ account.email = "test@k9mail.example"
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.accountName).isEqualTo("test@k9mail.example")
+ }
+
+ @Test
+ fun `account description is null`() {
+ account.description = null
+ account.email = "test@k9mail.example"
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.accountName).isEqualTo("test@k9mail.example")
+ }
+
+ @Test
+ fun `group key`() {
+ account.accountNumber = 42
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.groupKey).isEqualTo("newMailNotifications-42")
+ }
+
+ @Test
+ fun `notification color`() {
+ account.chipColor = 0xFF0000
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.color).isEqualTo(0xFF0000)
+ }
+
+ @Test
+ fun `new messages count`() {
+ val notificationData = createNotificationData(senders = listOf("irrelevant", "irrelevant"))
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.newMessagesCount).isEqualTo(2)
+ }
+
+ @Test
+ fun `do not display notification on lock screen`() {
+ setLockScreenMode(LockScreenNotificationVisibility.NOTHING)
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.lockScreenNotificationData).isEqualTo(LockScreenNotificationData.None)
+ }
+
+ @Test
+ fun `display application name on lock screen`() {
+ setLockScreenMode(LockScreenNotificationVisibility.APP_NAME)
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.lockScreenNotificationData).isEqualTo(LockScreenNotificationData.AppName)
+ }
+
+ @Test
+ fun `display new message count on lock screen`() {
+ setLockScreenMode(LockScreenNotificationVisibility.MESSAGE_COUNT)
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.lockScreenNotificationData).isEqualTo(LockScreenNotificationData.MessageCount)
+ }
+
+ @Test
+ fun `display message sender names on lock screen`() {
+ setLockScreenMode(LockScreenNotificationVisibility.SENDERS)
+ val notificationData = createNotificationData(senders = listOf("Sender One", "Sender Two", "Sender Three"))
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.lockScreenNotificationData).isInstanceOf(LockScreenNotificationData.SenderNames::class.java)
+ val senderNamesData = result.lockScreenNotificationData as LockScreenNotificationData.SenderNames
+ assertThat(senderNamesData.senderNames).isEqualTo("Sender One, Sender Two, Sender Three")
+ }
+
+ @Test
+ fun `display notification on lock screen`() {
+ setLockScreenMode(LockScreenNotificationVisibility.EVERYTHING)
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.lockScreenNotificationData).isEqualTo(LockScreenNotificationData.Public)
+ }
+
+ @Test
+ fun ringtone() {
+ account.notificationSetting.ringtone = "content://ringtone/1"
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.appearance.ringtone).isEqualTo("content://ringtone/1")
+ }
+
+ @Test
+ fun `vibration pattern`() {
+ account.notificationSetting.isVibrateEnabled = true
+ account.notificationSetting.vibratePattern = 3
+ account.notificationSetting.vibrateTimes = 2
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.appearance.vibrationPattern).isEqualTo(NotificationSetting.getVibration(3, 2))
+ }
+
+ @Test
+ fun `led color`() {
+ account.notificationSetting.ledColor = 0x00FF00
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createBaseNotificationData(notificationData)
+
+ assertThat(result.appearance.ledColor).isEqualTo(0x00FF00)
+ }
+
+ private fun setLockScreenMode(mode: LockScreenNotificationVisibility) {
+ K9.lockScreenNotificationVisibility = mode
+ }
+
+ private fun createNotificationData(senders: List = emptyList()): NotificationData {
+ val activeNotifications = senders.mapIndexed { index, sender ->
+ NotificationHolder(
+ notificationId = index,
+ timestamp = 0L,
+ content = NotificationContent(
+ messageReference = mock(),
+ sender = sender,
+ preview = "irrelevant",
+ summary = "irrelevant",
+ subject = "irrelevant"
+ )
+ )
+ }
+ return NotificationData(account, activeNotifications, inactiveNotifications = emptyList())
+ }
+
+ private fun createAccount(): Account {
+ return Account("00000000-0000-4000-0000-000000000000").apply {
+ description = "account name"
+ identities = listOf(Identity())
+ }
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationsTest.java b/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationsTest.java
deleted file mode 100644
index 59624902ff407ade31551cfb9af4ef25ca6e9342..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationsTest.java
+++ /dev/null
@@ -1,144 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import androidx.core.app.NotificationCompat.BigTextStyle;
-import androidx.core.app.NotificationCompat.Builder;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.K9.NotificationQuickDelete;
-import com.fsck.k9.testing.MockHelper;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-
-public class BaseNotificationsTest {
- private static final int ACCOUNT_COLOR = 0xAABBCC;
- private static final String ACCOUNT_NAME = "AccountName";
- private static final int ACCOUNT_NUMBER = 2;
- private static final String NOTIFICATION_SUMMARY = "Summary";
- private static final String SENDER = "MessageSender";
- private static final String SUBJECT = "Subject";
- private static final String NOTIFICATION_PREVIEW = "Preview";
-
-
- private NotificationResourceProvider resourceProvider = new TestNotificationResourceProvider();
- private TestNotifications notifications;
-
-
- @Before
- public void setUp() throws Exception {
- notifications = createTestNotifications();
- }
-
- @Test
- public void testCreateAndInitializeNotificationBuilder() throws Exception {
- Account account = createFakeAccount();
-
- Builder builder = notifications.createAndInitializeNotificationBuilder(account);
-
- verify(builder).setSmallIcon(resourceProvider.getIconNewMail());
- verify(builder).setColor(ACCOUNT_COLOR);
- verify(builder).setAutoCancel(true);
- }
-
- @Test
- public void testIsDeleteActionEnabled_NotificationQuickDelete_ALWAYS() throws Exception {
- K9.setNotificationQuickDeleteBehaviour(NotificationQuickDelete.ALWAYS);
-
- boolean result = notifications.isDeleteActionEnabled();
-
- assertTrue(result);
- }
-
- @Test
- public void testIsDeleteActionEnabled_NotificationQuickDelete_FOR_SINGLE_MSG() throws Exception {
- K9.setNotificationQuickDeleteBehaviour(NotificationQuickDelete.FOR_SINGLE_MSG);
-
- boolean result = notifications.isDeleteActionEnabled();
-
- assertTrue(result);
- }
-
- @Test
- public void testIsDeleteActionEnabled_NotificationQuickDelete_NEVER() throws Exception {
- K9.setNotificationQuickDeleteBehaviour(NotificationQuickDelete.NEVER);
-
- boolean result = notifications.isDeleteActionEnabled();
-
- assertFalse(result);
- }
-
- @Test
- public void testCreateBigTextStyleNotification() throws Exception {
- Account account = createFakeAccount();
- int notificationId = 23;
- NotificationHolder holder = createNotificationHolder(notificationId);
-
- Builder builder = notifications.createBigTextStyleNotification(account, holder, notificationId);
-
- verify(builder).setTicker(NOTIFICATION_SUMMARY);
- verify(builder).setGroup("newMailNotifications-" + ACCOUNT_NUMBER);
- verify(builder).setContentTitle(SENDER);
- verify(builder).setContentText(SUBJECT);
- verify(builder).setSubText(ACCOUNT_NAME);
-
- BigTextStyle bigTextStyle = notifications.bigTextStyle;
- verify(bigTextStyle).bigText(NOTIFICATION_PREVIEW);
-
- verify(builder).setStyle(bigTextStyle);
- }
-
- private NotificationHolder createNotificationHolder(int notificationId) {
- NotificationContent content = new NotificationContent(null, SENDER, SUBJECT, NOTIFICATION_PREVIEW,
- NOTIFICATION_SUMMARY, false);
- return new NotificationHolder(notificationId, content);
- }
-
- private TestNotifications createTestNotifications() {
- NotificationHelper notificationHelper = createFakeNotificationHelper();
- NotificationActionCreator actionCreator = mock(NotificationActionCreator.class);
-
- return new TestNotifications(notificationHelper, actionCreator, resourceProvider);
- }
-
- private NotificationHelper createFakeNotificationHelper() {
- Builder builder = MockHelper.mockBuilder(Builder.class);
- NotificationHelper notificationHelper = mock(NotificationHelper.class);
- when(notificationHelper.createNotificationBuilder(any(Account.class), any(NotificationChannelManager
- .ChannelType.class))).thenReturn(builder);
- when(notificationHelper.getAccountName(any(Account.class))).thenReturn(ACCOUNT_NAME);
- return notificationHelper;
- }
-
- private Account createFakeAccount() {
- Account account = mock(Account.class);
- when(account.getAccountNumber()).thenReturn(ACCOUNT_NUMBER);
- when(account.getChipColor()).thenReturn(ACCOUNT_COLOR);
- return account;
- }
-
-
- static class TestNotifications extends BaseNotifications {
-
- BigTextStyle bigTextStyle;
-
- protected TestNotifications(NotificationHelper notificationHelper, NotificationActionCreator actionCreator,
- NotificationResourceProvider resourceProvider) {
- super(notificationHelper, actionCreator, resourceProvider);
- bigTextStyle = mock(BigTextStyle.class);
- }
-
- @Override
- protected BigTextStyle createBigTextStyle(Builder builder) {
- return bigTextStyle;
- }
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/CertificateErrorNotificationControllerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/CertificateErrorNotificationControllerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f853d1fa68edffd5978cf8e4f8e04209ce79ea7d
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/CertificateErrorNotificationControllerTest.kt
@@ -0,0 +1,122 @@
+package com.fsck.k9.notification
+
+import android.app.Notification
+import android.app.PendingIntent
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.core.app.ApplicationProvider
+import com.fsck.k9.Account
+import com.fsck.k9.RobolectricTest
+import com.fsck.k9.testing.MockHelper.mockBuilder
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+
+private const val INCOMING = true
+private const val OUTGOING = false
+private const val ACCOUNT_NUMBER = 1
+private const val ACCOUNT_NAME = "TestAccount"
+
+class CertificateErrorNotificationControllerTest : RobolectricTest() {
+ private val resourceProvider: NotificationResourceProvider = TestNotificationResourceProvider()
+ private val notification = mock()
+ private val lockScreenNotification = mock()
+ private val notificationManager = mock()
+ private val builder = createFakeNotificationBuilder(notification)
+ private val lockScreenNotificationBuilder = createFakeNotificationBuilder(lockScreenNotification)
+ private val notificationHelper = createFakeNotificationHelper(
+ notificationManager,
+ builder,
+ lockScreenNotificationBuilder
+ )
+ private val account = createFakeAccount()
+ private val controller = TestCertificateErrorNotificationController()
+ private val contentIntent = mock()
+
+ @Test
+ fun testShowCertificateErrorNotificationForIncomingServer() {
+ val notificationId = NotificationIds.getCertificateErrorNotificationId(account, INCOMING)
+
+ controller.showCertificateErrorNotification(account, INCOMING)
+
+ verify(notificationManager).notify(notificationId, notification)
+ assertCertificateErrorNotificationContents()
+ }
+
+ @Test
+ fun testClearCertificateErrorNotificationsForIncomingServer() {
+ val notificationId = NotificationIds.getCertificateErrorNotificationId(account, INCOMING)
+
+ controller.clearCertificateErrorNotifications(account, INCOMING)
+
+ verify(notificationManager).cancel(notificationId)
+ }
+
+ @Test
+ fun testShowCertificateErrorNotificationForOutgoingServer() {
+ val notificationId = NotificationIds.getCertificateErrorNotificationId(account, OUTGOING)
+
+ controller.showCertificateErrorNotification(account, OUTGOING)
+
+ verify(notificationManager).notify(notificationId, notification)
+ assertCertificateErrorNotificationContents()
+ }
+
+ @Test
+ fun testClearCertificateErrorNotificationsForOutgoingServer() {
+ val notificationId = NotificationIds.getCertificateErrorNotificationId(account, OUTGOING)
+
+ controller.clearCertificateErrorNotifications(account, OUTGOING)
+
+ verify(notificationManager).cancel(notificationId)
+ }
+
+ private fun assertCertificateErrorNotificationContents() {
+ verify(builder).setSmallIcon(resourceProvider.iconWarning)
+ verify(builder).setTicker("Certificate error for $ACCOUNT_NAME")
+ verify(builder).setContentTitle("Certificate error for $ACCOUNT_NAME")
+ verify(builder).setContentText("Check your server settings")
+ verify(builder).setContentIntent(contentIntent)
+ verify(builder).setPublicVersion(lockScreenNotification)
+ verify(lockScreenNotificationBuilder).setContentTitle("Certificate error")
+ verify(lockScreenNotificationBuilder, never()).setContentText(any())
+ verify(lockScreenNotificationBuilder, never()).setTicker(any())
+ }
+
+ private fun createFakeNotificationBuilder(notification: Notification): NotificationCompat.Builder {
+ return mockBuilder {
+ on { build() } doReturn notification
+ }
+ }
+
+ private fun createFakeNotificationHelper(
+ notificationManager: NotificationManagerCompat,
+ notificationBuilder: NotificationCompat.Builder,
+ lockScreenNotificationBuilder: NotificationCompat.Builder
+ ): NotificationHelper {
+ return mock {
+ on { getContext() } doReturn ApplicationProvider.getApplicationContext()
+ on { getNotificationManager() } doReturn notificationManager
+ on { createNotificationBuilder(any(), any()) }.doReturn(notificationBuilder, lockScreenNotificationBuilder)
+ }
+ }
+
+ private fun createFakeAccount(): Account {
+ return mock {
+ on { accountNumber } doReturn ACCOUNT_NUMBER
+ on { description } doReturn ACCOUNT_NAME
+ on { uuid } doReturn "test-uuid"
+ }
+ }
+
+ internal inner class TestCertificateErrorNotificationController : CertificateErrorNotificationController(
+ notificationHelper, mock(), resourceProvider
+ ) {
+ override fun createContentIntent(account: Account, incoming: Boolean): PendingIntent {
+ return contentIntent
+ }
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/CertificateErrorNotificationsTest.java b/app/core/src/test/java/com/fsck/k9/notification/CertificateErrorNotificationsTest.java
deleted file mode 100644
index c739b9395ff60c2765803b35092099eaf1097754..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/CertificateErrorNotificationsTest.java
+++ /dev/null
@@ -1,149 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.Builder;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.testing.MockHelper;
-import com.fsck.k9.RobolectricTest;
-import org.junit.Before;
-import org.junit.Test;
-import org.robolectric.RuntimeEnvironment;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-
-public class CertificateErrorNotificationsTest extends RobolectricTest {
- private static final boolean INCOMING = true;
- private static final boolean OUTGOING = false;
- private static final int ACCOUNT_NUMBER = 1;
- private static final String ACCOUNT_NAME = "TestAccount";
-
-
- private NotificationResourceProvider resourceProvider = new TestNotificationResourceProvider();
- private Notification notification;
- private NotificationManagerCompat notificationManager;
- private NotificationCompat.Builder builder;
- private NotificationHelper notificationHelper;
- private Account account;
- private CertificateErrorNotifications certificateErrorNotifications;
- private PendingIntent contentIntent;
-
-
- @Before
- public void setUp() throws Exception {
- notification = createFakeNotification();
- notificationManager = createFakeNotificationManager();
- builder = createFakeNotificationBuilder(notification);
- notificationHelper = createFakeNotificationHelper(notificationManager, builder);
- account = createFakeAccount();
- contentIntent = createFakeContentIntent();
-
- certificateErrorNotifications = new TestCertificateErrorNotifications();
- }
-
- @Test
- public void testShowCertificateErrorNotificationForIncomingServer() throws Exception {
- int notificationId = NotificationIds.getCertificateErrorNotificationId(account, INCOMING);
-
- certificateErrorNotifications.showCertificateErrorNotification(account, INCOMING);
-
- verify(notificationManager).notify(notificationId, notification);
- assertCertificateErrorNotificationContents();
- }
-
- @Test
- public void testClearCertificateErrorNotificationsForIncomingServer() throws Exception {
- int notificationId = NotificationIds.getCertificateErrorNotificationId(account, INCOMING);
-
- certificateErrorNotifications.clearCertificateErrorNotifications(account, INCOMING);
-
- verify(notificationManager).cancel(notificationId);
- }
-
- @Test
- public void testShowCertificateErrorNotificationForOutgoingServer() throws Exception {
- int notificationId = NotificationIds.getCertificateErrorNotificationId(account, OUTGOING);
-
- certificateErrorNotifications.showCertificateErrorNotification(account, OUTGOING);
-
- verify(notificationManager).notify(notificationId, notification);
- assertCertificateErrorNotificationContents();
- }
-
- @Test
- public void testClearCertificateErrorNotificationsForOutgoingServer() throws Exception {
- int notificationId = NotificationIds.getCertificateErrorNotificationId(account, OUTGOING);
-
- certificateErrorNotifications.clearCertificateErrorNotifications(account, OUTGOING);
-
- verify(notificationManager).cancel(notificationId);
- }
-
- private void assertCertificateErrorNotificationContents() {
- verify(builder).setSmallIcon(resourceProvider.getIconWarning());
- verify(builder).setTicker("Certificate error for " + ACCOUNT_NAME);
- verify(builder).setContentTitle("Certificate error for " + ACCOUNT_NAME);
- verify(builder).setContentText("Check your server settings");
- verify(builder).setContentIntent(contentIntent);
- verify(builder).setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
- }
-
- private Notification createFakeNotification() {
- return mock(Notification.class);
- }
-
- private NotificationManagerCompat createFakeNotificationManager() {
- return mock(NotificationManagerCompat.class);
- }
-
- private Builder createFakeNotificationBuilder(Notification notification) {
- Builder builder = MockHelper.mockBuilder(Builder.class);
- when(builder.build()).thenReturn(notification);
- return builder;
- }
-
- private NotificationHelper createFakeNotificationHelper(NotificationManagerCompat notificationManager,
- NotificationCompat.Builder builder) {
- NotificationHelper notificationHelper = mock(NotificationHelper.class);
- when(notificationHelper.getContext()).thenReturn(RuntimeEnvironment.application);
- when(notificationHelper.getNotificationManager()).thenReturn(notificationManager);
- when(notificationHelper.createNotificationBuilder(any(Account.class),
- any(NotificationChannelManager.ChannelType.class)))
- .thenReturn(builder);
-
- return notificationHelper;
- }
-
- private Account createFakeAccount() {
- Account account = mock(Account.class);
- when(account.getAccountNumber()).thenReturn(ACCOUNT_NUMBER);
- when(account.getDescription()).thenReturn(ACCOUNT_NAME);
- when(account.getUuid()).thenReturn("test-uuid");
-
- return account;
- }
-
- private PendingIntent createFakeContentIntent() {
- return mock(PendingIntent.class);
- }
-
-
- class TestCertificateErrorNotifications extends CertificateErrorNotifications {
- public TestCertificateErrorNotifications() {
- super(notificationHelper, mock(NotificationActionCreator.class), resourceProvider);
- }
-
- @Override
- PendingIntent createContentIntent(Account account, boolean incoming) {
- return contentIntent;
- }
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/DeviceNotificationsTest.java b/app/core/src/test/java/com/fsck/k9/notification/DeviceNotificationsTest.java
deleted file mode 100644
index f0f0fb185ed29b69f52c61a64bdfc9b2b69cdc8d..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/DeviceNotificationsTest.java
+++ /dev/null
@@ -1,272 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.Arrays;
-import java.util.List;
-
-import android.app.Application;
-import android.app.Notification;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.BigTextStyle;
-import androidx.core.app.NotificationCompat.Builder;
-import androidx.core.app.NotificationCompat.InboxStyle;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.K9.NotificationHideSubject;
-import com.fsck.k9.K9.NotificationQuickDelete;
-import com.fsck.k9.NotificationSetting;
-import com.fsck.k9.RobolectricTest;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-import org.robolectric.RuntimeEnvironment;
-
-import static com.fsck.k9.testing.MockHelper.mockBuilder;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-
-public class DeviceNotificationsTest extends RobolectricTest {
- private static final int UNREAD_MESSAGE_COUNT = 42;
- private static final int NEW_MESSAGE_COUNT = 2;
- private static final String ACCOUNT_NAME = "accountName";
- private static final int ACCOUNT_NUMBER = 3;
- private static final int ACCOUNT_COLOR = 0xABCDEF;
- private static final String SUMMARY = "summary";
- private static final String PREVIEW = "preview";
- private static final String SUBJECT = "subject";
- private static final String SENDER = "sender";
- private static final String SUMMARY_2 = "summary2";
- private static final String PREVIEW_2 = "preview2";
- private static final String SUBJECT_2 = "subject2";
- private static final String SENDER_2 = "sender2";
- private static final int NOTIFICATION_ID = 23;
- private static final Notification FAKE_NOTIFICATION = mock(Notification.class);
-
-
- private NotificationResourceProvider resourceProvider = new TestNotificationResourceProvider();
- private Account account;
- private NotificationData notificationData;
- private TestDeviceNotifications notifications;
- private Builder builder;
- private Builder builder2 = mockBuilder(Builder.class);
- private LockScreenNotification lockScreenNotification;
-
-
- @Before
- public void setUp() throws Exception {
- account = createFakeAccount();
- notificationData = createFakeNotificationData(account);
-
- builder = createFakeNotificationBuilder();
- lockScreenNotification = mock(LockScreenNotification.class);
- notifications = createDeviceNotifications(builder, lockScreenNotification);
- }
-
- @Test
- public void buildSummaryNotification_withPrivacyModeActive() throws Exception {
- K9.setNotificationHideSubject(NotificationHideSubject.ALWAYS);
-
- Notification result = notifications.buildSummaryNotification(account, notificationData, false);
-
- verify(builder).setSmallIcon(resourceProvider.getIconNewMail());
- verify(builder).setColor(ACCOUNT_COLOR);
- verify(builder).setAutoCancel(true);
- verify(builder).setNumber(UNREAD_MESSAGE_COUNT);
- verify(builder).setTicker("New mail");
- verify(builder).setContentText("New mail");
- verify(builder).setContentTitle(UNREAD_MESSAGE_COUNT + " Unread (" + ACCOUNT_NAME + ")");
- verify(lockScreenNotification).configureLockScreenNotification(builder, notificationData);
- assertEquals(FAKE_NOTIFICATION, result);
- }
-
- @Test
- public void buildSummaryNotification_withSingleMessageNotification() throws Exception {
- K9.setNotificationHideSubject(NotificationHideSubject.NEVER);
- K9.setNotificationQuickDeleteBehaviour(NotificationQuickDelete.ALWAYS);
- when(notificationData.isSingleMessageNotification()).thenReturn(true);
-
- Notification result = notifications.buildSummaryNotification(account, notificationData, false);
-
- verify(builder).setSmallIcon(resourceProvider.getIconNewMail());
- verify(builder).setColor(ACCOUNT_COLOR);
- verify(builder).setAutoCancel(true);
- verify(builder).setTicker(SUMMARY);
- verify(builder).setContentText(SUBJECT);
- verify(builder).setContentTitle(SENDER);
- verify(builder).setStyle(notifications.bigTextStyle);
- verify(notifications.bigTextStyle).bigText(PREVIEW);
- verify(builder).addAction(resourceProvider.getIconReply(), "Reply", null);
- verify(builder).addAction(resourceProvider.getIconMarkAsRead(), "Mark Read", null);
- verify(builder).addAction(resourceProvider.getIconDelete(), "Delete", null);
- verify(lockScreenNotification).configureLockScreenNotification(builder, notificationData);
- assertEquals(FAKE_NOTIFICATION, result);
- }
-
- @Test
- public void buildSummaryNotification_withMultiMessageNotification() throws Exception {
- K9.setNotificationHideSubject(NotificationHideSubject.NEVER);
- K9.setNotificationQuickDeleteBehaviour(NotificationQuickDelete.ALWAYS);
- when(notificationData.isSingleMessageNotification()).thenReturn(false);
- when(notificationData.containsStarredMessages()).thenReturn(true);
-
- Notification result = notifications.buildSummaryNotification(account, notificationData, false);
-
- verify(builder).setSmallIcon(resourceProvider.getIconNewMail());
- verify(builder).setColor(ACCOUNT_COLOR);
- verify(builder).setAutoCancel(true);
- verify(builder).setTicker(SUMMARY);
- verify(builder).setContentTitle(NEW_MESSAGE_COUNT + " new messages");
- verify(builder).setSubText(ACCOUNT_NAME);
- verify(builder).setGroup("newMailNotifications-" + ACCOUNT_NUMBER);
- verify(builder).setGroupSummary(true);
- verify(builder).setPriority(NotificationCompat.PRIORITY_HIGH);
- verify(builder).setStyle(notifications.inboxStyle);
- verify(notifications.inboxStyle).setBigContentTitle(NEW_MESSAGE_COUNT + " new messages");
- verify(notifications.inboxStyle).setSummaryText(ACCOUNT_NAME);
- verify(notifications.inboxStyle).addLine(SUMMARY);
- verify(notifications.inboxStyle).addLine(SUMMARY_2);
- verify(builder).addAction(resourceProvider.getIconMarkAsRead(), "Mark Read", null);
- verify(builder).addAction(resourceProvider.getIconDelete(), "Delete", null);
- verify(lockScreenNotification).configureLockScreenNotification(builder, notificationData);
- assertEquals(FAKE_NOTIFICATION, result);
- }
-
- @Test
- public void buildSummaryNotification_withAdditionalMessages() throws Exception {
- K9.setNotificationHideSubject(NotificationHideSubject.NEVER);
- K9.setNotificationQuickDeleteBehaviour(NotificationQuickDelete.ALWAYS);
- when(notificationData.isSingleMessageNotification()).thenReturn(false);
- when(notificationData.hasSummaryOverflowMessages()).thenReturn(true);
- when(notificationData.getSummaryOverflowMessagesCount()).thenReturn(23);
-
- notifications.buildSummaryNotification(account, notificationData, false);
-
- verify(notifications.inboxStyle).setSummaryText("+ 23 more on " + ACCOUNT_NAME);
- }
-
- @Test
- public void buildSummaryNotification_withoutDeleteAllAction() throws Exception {
- K9.setNotificationHideSubject(NotificationHideSubject.NEVER);
- K9.setNotificationQuickDeleteBehaviour(NotificationQuickDelete.NEVER);
- when(notificationData.isSingleMessageNotification()).thenReturn(false);
-
- notifications.buildSummaryNotification(account, notificationData, false);
-
- verify(builder, never()).addAction(resourceProvider.getIconDelete(), "Delete", null);
- }
-
- @Test
- public void buildSummaryNotification_withoutDeleteAction() throws Exception {
- K9.setNotificationHideSubject(NotificationHideSubject.NEVER);
- K9.setNotificationQuickDeleteBehaviour(NotificationQuickDelete.NEVER);
- when(notificationData.isSingleMessageNotification()).thenReturn(true);
-
- notifications.buildSummaryNotification(account, notificationData, false);
-
- verify(builder, never()).addAction(resourceProvider.getIconDelete(), "Delete", null);
- }
-
- private Builder createFakeNotificationBuilder() {
- Builder builder = mockBuilder(Builder.class);
- when(builder.build()).thenReturn(FAKE_NOTIFICATION);
- return builder;
- }
-
- private Account createFakeAccount() {
- Account account = mock(Account.class);
-
- when(account.getChipColor()).thenReturn(ACCOUNT_COLOR);
- when(account.getAccountNumber()).thenReturn(ACCOUNT_NUMBER);
-
- NotificationSetting notificationSetting = mock(NotificationSetting.class);
- when(account.getNotificationSetting()).thenReturn(notificationSetting);
-
- return account;
- }
-
- private NotificationData createFakeNotificationData(Account account) {
- NotificationData notificationData = mock(NotificationData.class);
- when(notificationData.getUnreadMessageCount()).thenReturn(UNREAD_MESSAGE_COUNT);
- when(notificationData.getNewMessagesCount()).thenReturn(NEW_MESSAGE_COUNT);
- when(notificationData.getAccount()).thenReturn(account);
-
- NotificationContent content = new NotificationContent(null, SENDER, SUBJECT, PREVIEW, SUMMARY, false);
- NotificationContent content2 = new NotificationContent(null, SENDER_2, SUBJECT_2, PREVIEW_2, SUMMARY_2, true);
- List contents = Arrays.asList(content, content2);
- when(notificationData.getContentForSummaryNotification()).thenReturn(contents);
-
- NotificationHolder holder = new NotificationHolder(NOTIFICATION_ID, content);
- when(notificationData.getHolderForLatestNotification()).thenReturn(holder);
-
- return notificationData;
- }
-
- private TestDeviceNotifications createDeviceNotifications(Builder builder,
- LockScreenNotification lockScreenNotification) {
- NotificationHelper notificationHelper = createFakeNotificationHelper(builder);
- NotificationActionCreator actionCreator = mock(NotificationActionCreator.class);
- WearNotifications wearNotifications = mock(WearNotifications.class);
-
- return new TestDeviceNotifications(notificationHelper, actionCreator, lockScreenNotification,
- wearNotifications, resourceProvider);
- }
-
- private NotificationHelper createFakeNotificationHelper(final Builder builder) {
- Application context = RuntimeEnvironment.application;
-
- NotificationHelper notificationHelper = mock(NotificationHelper.class);
- when(notificationHelper.getContext()).thenReturn(context);
- when(notificationHelper.getAccountName(any(Account.class))).thenReturn(ACCOUNT_NAME);
- when(notificationHelper.createNotificationBuilder(any(Account.class), any(NotificationChannelManager
- .ChannelType.class))).thenAnswer(new Answer() {
- private int invocationCount = 0;
-
- @Override
- public Builder answer(InvocationOnMock invocation) throws Throwable {
- invocationCount++;
- switch (invocationCount) {
- case 1: {
- return builder;
- }
- case 2: {
- return builder2;
- }
- }
-
- throw new AssertionError("createNotificationBuilder() invoked more than twice");
- }
- });
-
- return notificationHelper;
- }
-
-
- static class TestDeviceNotifications extends DeviceNotifications {
- BigTextStyle bigTextStyle = mockBuilder(BigTextStyle.class);
- InboxStyle inboxStyle = mockBuilder(InboxStyle.class);
-
-
- TestDeviceNotifications(NotificationHelper notificationHelper, NotificationActionCreator actionCreator,
- LockScreenNotification lockScreenNotification, WearNotifications wearNotifications,
- NotificationResourceProvider resourceProvider) {
- super(notificationHelper, actionCreator, lockScreenNotification, wearNotifications, resourceProvider);
- }
-
- @Override
- protected BigTextStyle createBigTextStyle(Builder builder) {
- return bigTextStyle;
- }
-
- @Override
- protected InboxStyle createInboxStyle(Builder builder) {
- return inboxStyle;
- }
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/LockScreenNotificationCreatorTest.kt b/app/core/src/test/java/com/fsck/k9/notification/LockScreenNotificationCreatorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f92ec2a8662769c88ba96d5725eca65c37fec990
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/LockScreenNotificationCreatorTest.kt
@@ -0,0 +1,116 @@
+package com.fsck.k9.notification
+
+import androidx.core.app.NotificationCompat
+import androidx.test.core.app.ApplicationProvider
+import com.fsck.k9.Account
+import com.fsck.k9.RobolectricTest
+import com.fsck.k9.testing.MockHelper.mockBuilder
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+class LockScreenNotificationCreatorTest : RobolectricTest() {
+ private val account = Account("00000000-0000-0000-0000-000000000000")
+ private val resourceProvider = TestNotificationResourceProvider()
+ private val builder = createFakeNotificationBuilder()
+ private val publicBuilder = createFakeNotificationBuilder()
+ private var notificationCreator = LockScreenNotificationCreator(
+ notificationHelper = createFakeNotificationHelper(publicBuilder),
+ resourceProvider = resourceProvider
+ )
+
+ @Test
+ fun `no lock screen notification`() {
+ val baseNotificationData = createBaseNotificationData(LockScreenNotificationData.None)
+
+ notificationCreator.configureLockScreenNotification(builder, baseNotificationData)
+
+ verify(builder).setVisibility(NotificationCompat.VISIBILITY_SECRET)
+ }
+
+ @Test
+ fun `app name`() {
+ val baseNotificationData = createBaseNotificationData(LockScreenNotificationData.AppName)
+
+ notificationCreator.configureLockScreenNotification(builder, baseNotificationData)
+
+ verify(builder).setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
+ }
+
+ @Test
+ fun `regular notification on lock screen`() {
+ val baseNotificationData = createBaseNotificationData(LockScreenNotificationData.Public)
+
+ notificationCreator.configureLockScreenNotification(builder, baseNotificationData)
+
+ verify(builder).setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ }
+
+ @Test
+ fun `list of sender names`() {
+ val baseNotificationData = createBaseNotificationData(
+ lockScreenNotificationData = LockScreenNotificationData.SenderNames("Alice, Bob"),
+ newMessagesCount = 2
+ )
+
+ notificationCreator.configureLockScreenNotification(builder, baseNotificationData)
+
+ verify(publicBuilder).setSmallIcon(resourceProvider.iconNewMail)
+ verify(publicBuilder).setNumber(2)
+ verify(publicBuilder).setContentTitle("2 new messages")
+ verify(publicBuilder).setContentText("Alice, Bob")
+ verify(builder).setPublicVersion(publicBuilder.build())
+ }
+
+ @Test
+ fun `new message count`() {
+ val baseNotificationData = createBaseNotificationData(
+ lockScreenNotificationData = LockScreenNotificationData.MessageCount,
+ accountName = "Account name",
+ newMessagesCount = 23
+ )
+
+ notificationCreator.configureLockScreenNotification(builder, baseNotificationData)
+
+ verify(publicBuilder).setSmallIcon(resourceProvider.iconNewMail)
+ verify(publicBuilder).setNumber(23)
+ verify(publicBuilder).setContentTitle("23 new messages")
+ verify(publicBuilder).setContentText("Account name")
+ verify(builder).setPublicVersion(publicBuilder.build())
+ }
+
+ private fun createFakeNotificationBuilder(): NotificationCompat.Builder {
+ return mockBuilder {
+ on { build() } doReturn mock()
+ }
+ }
+
+ private fun createFakeNotificationHelper(builder: NotificationCompat.Builder): NotificationHelper {
+ return mock {
+ on { getContext() } doReturn ApplicationProvider.getApplicationContext()
+ on { createNotificationBuilder(any(), any()) } doReturn builder
+ }
+ }
+
+ private fun createBaseNotificationData(
+ lockScreenNotificationData: LockScreenNotificationData,
+ accountName: String = "irrelevant",
+ newMessagesCount: Int = 0
+ ): BaseNotificationData {
+ return BaseNotificationData(
+ account = account,
+ accountName = accountName,
+ groupKey = "irrelevant",
+ color = 0,
+ newMessagesCount = newMessagesCount,
+ lockScreenNotificationData = lockScreenNotificationData,
+ appearance = NotificationAppearance(
+ ringtone = null,
+ vibrationPattern = longArrayOf(),
+ ledColor = 0
+ )
+ )
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/LockScreenNotificationTest.java b/app/core/src/test/java/com/fsck/k9/notification/LockScreenNotificationTest.java
deleted file mode 100644
index 8de5dc4c6d0b5930f7a03e2f704bd3ec675765e3..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/LockScreenNotificationTest.java
+++ /dev/null
@@ -1,187 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.Arrays;
-
-import android.app.Notification;
-import android.content.Context;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.Builder;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.K9.LockScreenNotificationVisibility;
-import com.fsck.k9.testing.MockHelper;
-import com.fsck.k9.RobolectricTest;
-import org.junit.Before;
-import org.junit.Test;
-import org.robolectric.RuntimeEnvironment;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-
-public class LockScreenNotificationTest extends RobolectricTest {
- private static final String ACCOUNT_NAME = "Hugo";
- private static final int NEW_MESSAGE_COUNT = 3;
- private static final int UNREAD_MESSAGE_COUNT = 4;
-
- private NotificationResourceProvider resourceProvider = new TestNotificationResourceProvider();
- private Builder builder;
- private Builder publicBuilder;
- private LockScreenNotification lockScreenNotification;
- private NotificationData notificationData;
-
-
- @Before
- public void setUp() throws Exception {
- Context context = RuntimeEnvironment.application;
- builder = createFakeNotificationBuilder();
- publicBuilder = createFakeNotificationBuilder();
- NotificationHelper notificationHelper = createFakeNotificationHelper(context, publicBuilder);
- Account account = createFakeAccount();
- notificationData = createFakeNotificationData(account);
- lockScreenNotification = new LockScreenNotification(notificationHelper, resourceProvider);
- }
-
- @Test
- public void configureLockScreenNotification_NOTHING() throws Exception {
- K9.setLockScreenNotificationVisibility(LockScreenNotificationVisibility.NOTHING);
-
- lockScreenNotification.configureLockScreenNotification(builder, notificationData);
-
- verify(builder).setVisibility(NotificationCompat.VISIBILITY_SECRET);
- }
-
- @Test
- public void configureLockScreenNotification_APP_NAME() throws Exception {
- K9.setLockScreenNotificationVisibility(LockScreenNotificationVisibility.APP_NAME);
-
- lockScreenNotification.configureLockScreenNotification(builder, notificationData);
-
- verify(builder).setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
- }
-
- @Test
- public void configureLockScreenNotification_EVERYTHING() throws Exception {
- K9.setLockScreenNotificationVisibility(LockScreenNotificationVisibility.EVERYTHING);
-
- lockScreenNotification.configureLockScreenNotification(builder, notificationData);
-
- verify(builder).setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
- }
-
- @Test
- public void configureLockScreenNotification_SENDERS_withSingleMessage() throws Exception {
- K9.setLockScreenNotificationVisibility(LockScreenNotificationVisibility.SENDERS);
- String senderName = "alice@example.com";
- NotificationContent content = createNotificationContent(senderName);
- NotificationHolder holder = new NotificationHolder(42, content);
- when(notificationData.getNewMessagesCount()).thenReturn(1);
- when(notificationData.getUnreadMessageCount()).thenReturn(1);
- when(notificationData.getHolderForLatestNotification()).thenReturn(holder);
-
- lockScreenNotification.configureLockScreenNotification(builder, notificationData);
-
- verify(publicBuilder).setSmallIcon(resourceProvider.getIconNewMail());
- verify(publicBuilder).setNumber(1);
- verify(publicBuilder).setContentTitle("1 new message");
- verify(publicBuilder).setContentText(senderName);
- verify(builder).setPublicVersion(publicBuilder.build());
- }
-
- @Test
- public void configureLockScreenNotification_SENDERS_withMultipleMessages() throws Exception {
- K9.setLockScreenNotificationVisibility(LockScreenNotificationVisibility.SENDERS);
- NotificationContent content1 = createNotificationContent("alice@example.com");
- NotificationContent content2 = createNotificationContent("Bob ");
- NotificationContent content3 = createNotificationContent("\"Peter Lustig\" ");
- when(notificationData.getNewMessagesCount()).thenReturn(NEW_MESSAGE_COUNT);
- when(notificationData.getUnreadMessageCount()).thenReturn(UNREAD_MESSAGE_COUNT);
- when(notificationData.getContentForSummaryNotification()).thenReturn(
- Arrays.asList(content1, content2, content3));
-
- lockScreenNotification.configureLockScreenNotification(builder, notificationData);
-
- verify(publicBuilder).setSmallIcon(resourceProvider.getIconNewMail());
- verify(publicBuilder).setNumber(UNREAD_MESSAGE_COUNT);
- verify(publicBuilder).setContentTitle(NEW_MESSAGE_COUNT + " new messages");
- verify(publicBuilder).setContentText(
- "alice@example.com, Bob , \"Peter Lustig\" ");
- verify(builder).setPublicVersion(publicBuilder.build());
- }
-
- @Test
- public void configureLockScreenNotification_SENDERS_makeSureWeGetEnoughSenderNames() throws Exception {
- assertTrue(NotificationData.MAX_NUMBER_OF_MESSAGES_FOR_SUMMARY_NOTIFICATION >=
- LockScreenNotification.MAX_NUMBER_OF_SENDERS_IN_LOCK_SCREEN_NOTIFICATION);
- }
-
- @Test
- public void createCommaSeparatedListOfSenders_withMoreSendersThanShouldBeDisplayed() throws Exception {
- NotificationContent content1 = createNotificationContent("alice@example.com");
- NotificationContent content2 = createNotificationContent("bob@example.com");
- NotificationContent content3 = createNotificationContent("cloe@example.com");
- NotificationContent content4 = createNotificationContent("dagobert@example.com");
- NotificationContent content5 = createNotificationContent("ed@example.com");
- NotificationContent content6 = createNotificationContent("fiona@example.com");
-
- String result = lockScreenNotification.createCommaSeparatedListOfSenders(
- Arrays.asList(content1, content2, content3, content4, content5, content6));
-
- assertEquals(
- "alice@example.com, bob@example.com, cloe@example.com, dagobert@example.com, ed@example.com", result);
- }
-
- @Test
- public void configureLockScreenNotification_MESSAGE_COUNT() throws Exception {
- K9.setLockScreenNotificationVisibility(LockScreenNotificationVisibility.MESSAGE_COUNT);
- when(notificationData.getNewMessagesCount()).thenReturn(NEW_MESSAGE_COUNT);
- when(notificationData.getUnreadMessageCount()).thenReturn(UNREAD_MESSAGE_COUNT);
-
- lockScreenNotification.configureLockScreenNotification(builder, notificationData);
-
- verify(publicBuilder).setSmallIcon(resourceProvider.getIconNewMail());
- verify(publicBuilder).setNumber(UNREAD_MESSAGE_COUNT);
- verify(publicBuilder).setContentTitle(NEW_MESSAGE_COUNT + " new messages");
- verify(publicBuilder).setContentText(ACCOUNT_NAME);
- verify(builder).setPublicVersion(publicBuilder.build());
- }
-
- private Account createFakeAccount() {
- Account account = mock(Account.class);
- when(account.getDescription()).thenReturn(ACCOUNT_NAME);
- return account;
- }
-
- private Builder createFakeNotificationBuilder() {
- Builder builder = MockHelper.mockBuilder(Builder.class);
- when(builder.build()).thenReturn(mock(Notification.class));
- return builder;
- }
-
- private NotificationHelper createFakeNotificationHelper(Context context, Builder builder) {
- NotificationHelper notificationHelper = mock(NotificationHelper.class);
- when(notificationHelper.getContext()).thenReturn(context);
- when(notificationHelper.getAccountName(any(Account.class))).thenReturn(ACCOUNT_NAME);
- when(notificationHelper.createNotificationBuilder(any(Account.class), any(NotificationChannelManager
- .ChannelType.class))).thenReturn(builder);
-
- return notificationHelper;
- }
-
- private NotificationData createFakeNotificationData(Account account) {
- NotificationData notificationData = mock(NotificationData.class);
- when(notificationData.getAccount()).thenReturn(account);
-
- return notificationData;
- }
-
- private NotificationContent createNotificationContent(String sender) {
- return new NotificationContent(null, sender, null, null, null, false);
- }
-}
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
new file mode 100644
index 0000000000000000000000000000000000000000..873dd7cb5f7f4f683e4ea070b14aabd7526e5d49
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt
@@ -0,0 +1,457 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.TestClock
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.mailstore.LocalMessage
+import com.fsck.k9.mailstore.LocalStore
+import com.fsck.k9.mailstore.LocalStoreProvider
+import com.fsck.k9.mailstore.MessageStoreManager
+import com.fsck.k9.mailstore.NotificationMessage
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
+import org.junit.Test
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stubbing
+
+private const val ACCOUNT_UUID = "00000000-0000-4000-0000-000000000000"
+private const val ACCOUNT_NAME = "Personal"
+private const val ACCOUNT_COLOR = 0xFF112233L.toInt()
+private const val FOLDER_ID = 42L
+private const val TIMESTAMP = 23L
+
+class NewMailNotificationManagerTest {
+ private val mockedNotificationMessages = mutableListOf()
+ private val account = createAccount()
+ private val notificationContentCreator = mock()
+ private val localStoreProvider = createLocalStoreProvider()
+ private val clock = TestClock(TIMESTAMP)
+ private val manager = NewMailNotificationManager(
+ notificationContentCreator,
+ createNotificationRepository(),
+ BaseNotificationDataCreator(),
+ SingleMessageNotificationDataCreator(),
+ SummaryNotificationDataCreator(SingleMessageNotificationDataCreator()),
+ clock
+ )
+
+ @Test
+ fun `add first notification`() {
+ val message = addMessageToNotificationContentCreator(
+ sender = "sender",
+ subject = "subject",
+ preview = "preview",
+ summary = "summary",
+ messageUid = "msg-1"
+ )
+
+ val result = manager.addNewMailNotification(account, message, silent = false)
+
+ assertThat(result.singleNotificationData.first().content).isEqualTo(
+ NotificationContent(
+ messageReference = createMessageReference("msg-1"),
+ sender = "sender",
+ subject = "subject",
+ preview = "preview",
+ summary = "summary"
+ )
+ )
+ assertThat(result.summaryNotificationData).isInstanceOf(SummarySingleNotificationData::class.java)
+ val summaryNotificationData = result.summaryNotificationData as SummarySingleNotificationData
+ assertThat(summaryNotificationData.singleNotificationData.isSilent).isFalse()
+ }
+
+ @Test
+ fun `add second notification`() {
+ val messageOne = addMessageToNotificationContentCreator(
+ sender = "Alice",
+ subject = "Hi Bob",
+ preview = "How are you?",
+ summary = "Alice Hi Bob",
+ messageUid = "msg-1"
+ )
+ val messageTwo = addMessageToNotificationContentCreator(
+ sender = "Zoe",
+ subject = "Meeting",
+ preview = "We need to talk",
+ summary = "Zoe Meeting",
+ messageUid = "msg-2"
+ )
+ manager.addNewMailNotification(account, messageOne, silent = false)
+ val timestamp = TIMESTAMP + 1000
+ clock.time = timestamp
+
+ val result = manager.addNewMailNotification(account, messageTwo, silent = false)
+
+ assertThat(result.singleNotificationData.first().content).isEqualTo(
+ NotificationContent(
+ messageReference = createMessageReference("msg-2"),
+ sender = "Zoe",
+ subject = "Meeting",
+ preview = "We need to talk",
+ summary = "Zoe Meeting"
+ )
+ )
+ assertThat(result.baseNotificationData.newMessagesCount).isEqualTo(2)
+ assertThat(result.summaryNotificationData).isInstanceOf(SummaryInboxNotificationData::class.java)
+ val summaryNotificationData = result.summaryNotificationData as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.content).isEqualTo(listOf("Zoe Meeting", "Alice Hi Bob"))
+ assertThat(summaryNotificationData.messageReferences).isEqualTo(
+ listOf(
+ createMessageReference("msg-2"),
+ createMessageReference("msg-1")
+ )
+ )
+ assertThat(summaryNotificationData.additionalMessagesCount).isEqualTo(0)
+ assertThat(summaryNotificationData.isSilent).isFalse()
+ }
+
+ @Test
+ fun `add one more notification when already displaying the maximum number of notifications`() {
+ addMaximumNumberOfNotifications()
+ val message = addMessageToNotificationContentCreator(
+ sender = "Alice",
+ subject = "Another one",
+ preview = "Are you tired of me yet?",
+ summary = "Alice Another one",
+ messageUid = "msg-x"
+ )
+
+ val result = manager.addNewMailNotification(account, message, silent = false)
+
+ val notificationId = NotificationIds.getSingleMessageNotificationId(account, index = 0)
+ assertThat(result.cancelNotificationIds).isEqualTo(listOf(notificationId))
+ assertThat(result.singleNotificationData.first().notificationId).isEqualTo(notificationId)
+ }
+
+ @Test
+ fun `remove notification when none was added before should return null`() {
+ val result = manager.removeNewMailNotifications(account, clearNewMessageState = true) {
+ listOf(createMessageReference("any"))
+ }
+
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun `remove notification with untracked notification ID should return null`() {
+ val message = addMessageToNotificationContentCreator(
+ sender = "Alice",
+ subject = "Another one",
+ preview = "Are you tired of me yet?",
+ summary = "Alice Another one",
+ messageUid = "msg-x"
+ )
+ manager.addNewMailNotification(account, message, silent = false)
+
+ val result = manager.removeNewMailNotifications(account, clearNewMessageState = true) {
+ listOf(createMessageReference("untracked"))
+ }
+
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun `remove last remaining notification`() {
+ val message = addMessageToNotificationContentCreator(
+ sender = "Alice",
+ subject = "Hello",
+ preview = "How are you?",
+ summary = "Alice Hello",
+ messageUid = "msg-1"
+ )
+ manager.addNewMailNotification(account, message, silent = false)
+
+ val result = manager.removeNewMailNotifications(account, clearNewMessageState = true) {
+ listOf(createMessageReference("msg-1"))
+ }
+
+ assertNotNull(result) { data ->
+ assertThat(data.cancelNotificationIds).containsExactly(
+ NotificationIds.getNewMailSummaryNotificationId(account),
+ NotificationIds.getSingleMessageNotificationId(account, 0)
+ )
+ assertThat(data.singleNotificationData).isEmpty()
+ assertThat(data.summaryNotificationData).isNull()
+ }
+ }
+
+ @Test
+ fun `remove one of three notifications`() {
+ val messageOne = addMessageToNotificationContentCreator(
+ sender = "Alice",
+ subject = "One",
+ preview = "preview",
+ summary = "Alice One",
+ messageUid = "msg-1"
+ )
+ manager.addNewMailNotification(account, messageOne, silent = false)
+ val messageTwo = addMessageToNotificationContentCreator(
+ sender = "Alice",
+ subject = "Two",
+ preview = "preview",
+ summary = "Alice Two",
+ messageUid = "msg-2"
+ )
+ val dataTwo = manager.addNewMailNotification(account, messageTwo, silent = true)
+ val notificationIdTwo = dataTwo.singleNotificationData.first().notificationId
+ val messageThree = addMessageToNotificationContentCreator(
+ sender = "Alice",
+ subject = "Three",
+ preview = "preview",
+ summary = "Alice Three",
+ messageUid = "msg-3"
+ )
+ manager.addNewMailNotification(account, messageThree, silent = true)
+
+ val result = manager.removeNewMailNotifications(account, clearNewMessageState = true) {
+ listOf(createMessageReference("msg-2"))
+ }
+
+ assertNotNull(result) { data ->
+ assertThat(data.cancelNotificationIds).isEqualTo(listOf(notificationIdTwo))
+ assertThat(data.singleNotificationData).isEmpty()
+ assertThat(data.baseNotificationData.newMessagesCount).isEqualTo(2)
+ assertThat(data.summaryNotificationData).isInstanceOf(SummaryInboxNotificationData::class.java)
+ val summaryNotificationData = data.summaryNotificationData as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.content).isEqualTo(listOf("Alice Three", "Alice One"))
+ assertThat(summaryNotificationData.messageReferences).isEqualTo(
+ listOf(
+ createMessageReference("msg-3"),
+ createMessageReference("msg-1")
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `remove notification when additional notifications are available`() {
+ val message = addMessageToNotificationContentCreator(
+ sender = "Alice",
+ subject = "Another one",
+ preview = "Are you tired of me yet?",
+ summary = "Alice Another one",
+ messageUid = "msg-restore"
+ )
+ manager.addNewMailNotification(account, message, silent = false)
+ addMaximumNumberOfNotifications()
+
+ val result = manager.removeNewMailNotifications(account, clearNewMessageState = true) {
+ listOf(createMessageReference("msg-1"))
+ }
+
+ assertNotNull(result) { data ->
+ assertThat(data.cancelNotificationIds).hasSize(1)
+ assertThat(data.baseNotificationData.newMessagesCount)
+ .isEqualTo(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS)
+
+ val singleNotificationData = data.singleNotificationData.first()
+ assertThat(singleNotificationData.notificationId).isEqualTo(data.cancelNotificationIds.first())
+ assertThat(singleNotificationData.isSilent).isTrue()
+ assertThat(singleNotificationData.content).isEqualTo(
+ NotificationContent(
+ messageReference = createMessageReference("msg-restore"),
+ sender = "Alice",
+ subject = "Another one",
+ preview = "Are you tired of me yet?",
+ summary = "Alice Another one"
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `restore notifications without persisted notifications`() {
+ val result = manager.restoreNewMailNotifications(account)
+
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun `restore notifications with single persisted notification`() {
+ addNotificationMessage(
+ notificationId = 10,
+ timestamp = 20L,
+ sender = "Sender",
+ subject = "Subject",
+ summary = "Summary",
+ preview = "Preview",
+ messageUid = "uid-1"
+ )
+
+ val result = manager.restoreNewMailNotifications(account)
+
+ assertNotNull(result) { data ->
+ assertThat(data.cancelNotificationIds).isEmpty()
+ assertThat(data.baseNotificationData.newMessagesCount).isEqualTo(1)
+ assertThat(data.singleNotificationData).hasSize(1)
+
+ val singleNotificationData = data.singleNotificationData.first()
+ assertThat(singleNotificationData.notificationId).isEqualTo(10)
+ assertThat(singleNotificationData.isSilent).isTrue()
+ assertThat(singleNotificationData.addLockScreenNotification).isTrue()
+ assertThat(singleNotificationData.content).isEqualTo(
+ NotificationContent(
+ messageReference = createMessageReference("uid-1"),
+ sender = "Sender",
+ subject = "Subject",
+ preview = "Preview",
+ summary = "Summary"
+ )
+ )
+
+ assertThat(data.summaryNotificationData).isInstanceOf(SummarySingleNotificationData::class.java)
+ val summaryNotificationData = data.summaryNotificationData as SummarySingleNotificationData
+ assertThat(summaryNotificationData.singleNotificationData.isSilent).isTrue()
+ assertThat(summaryNotificationData.singleNotificationData.content).isEqualTo(
+ NotificationContent(
+ messageReference = createMessageReference("uid-1"),
+ sender = "Sender",
+ subject = "Subject",
+ preview = "Preview",
+ summary = "Summary"
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `restore notifications with one inactive persisted notification`() {
+ addMaximumNumberOfNotificationMessages()
+ addNotificationMessage(
+ notificationId = null,
+ timestamp = 1000L,
+ sender = "inactive",
+ subject = "inactive",
+ summary = "inactive",
+ preview = "inactive",
+ messageUid = "uid-inactive"
+ )
+
+ val result = manager.restoreNewMailNotifications(account)
+
+ assertNotNull(result) { data ->
+ assertThat(data.cancelNotificationIds).isEmpty()
+ assertThat(data.baseNotificationData.newMessagesCount)
+ .isEqualTo(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS + 1)
+ assertThat(data.singleNotificationData).hasSize(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS)
+ assertThat(data.singleNotificationData.map { it.content.sender }).doesNotContain("inactive")
+
+ assertThat(data.summaryNotificationData).isInstanceOf(SummaryInboxNotificationData::class.java)
+ val summaryNotificationData = data.summaryNotificationData as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.isSilent).isTrue()
+ }
+ }
+
+ private fun createAccount(): Account {
+ return Account(ACCOUNT_UUID).apply {
+ description = ACCOUNT_NAME
+ chipColor = ACCOUNT_COLOR
+ }
+ }
+
+ private fun addMaximumNumberOfNotifications() {
+ repeat(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS) { index ->
+ val message = addMessageToNotificationContentCreator(
+ sender = "sender",
+ subject = "subject",
+ preview = "preview",
+ summary = "summary",
+ messageUid = "msg-$index"
+ )
+ manager.addNewMailNotification(account, message, silent = true)
+ }
+ }
+
+ private fun addMessageToNotificationContentCreator(
+ sender: String,
+ subject: String,
+ preview: String,
+ summary: String,
+ messageUid: String
+ ): LocalMessage {
+ val message = mock()
+
+ stubbing(notificationContentCreator) {
+ on { createFromMessage(account, message) } doReturn
+ NotificationContent(
+ messageReference = createMessageReference(messageUid),
+ sender, subject, preview, summary
+ )
+ }
+
+ return message
+ }
+
+ private fun addNotificationMessage(
+ notificationId: Int?,
+ timestamp: Long,
+ sender: String,
+ subject: String,
+ preview: String,
+ summary: String,
+ messageUid: String
+ ) {
+ val message = mock()
+
+ val notificationMessage = NotificationMessage(message, notificationId, timestamp)
+ mockedNotificationMessages.add(notificationMessage)
+
+ stubbing(notificationContentCreator) {
+ on { createFromMessage(account, message) } doReturn
+ NotificationContent(
+ messageReference = createMessageReference(messageUid),
+ sender, subject, preview, summary
+ )
+ }
+ }
+
+ private fun addMaximumNumberOfNotificationMessages() {
+ repeat(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS) { index ->
+ addNotificationMessage(
+ notificationId = index,
+ timestamp = index.toLong(),
+ sender = "irrelevant",
+ subject = "irrelevant",
+ preview = "irrelevant",
+ summary = "irrelevant",
+ messageUid = "uid-$index"
+ )
+ }
+ }
+
+ private fun createMessageReference(messageUid: String): MessageReference {
+ return MessageReference(ACCOUNT_UUID, FOLDER_ID, messageUid)
+ }
+
+ private fun createLocalStoreProvider(): LocalStoreProvider {
+ val localStore = createLocalStore()
+ return mock {
+ on { getInstance(account) } doReturn localStore
+ }
+ }
+
+ private fun createLocalStore(): LocalStore {
+ return mock {
+ on { notificationMessages } doAnswer { mockedNotificationMessages.toList() }
+ }
+ }
+
+ private fun createNotificationRepository(): NotificationRepository {
+ val notificationStoreProvider = mock {
+ on { getNotificationStore(account) } doReturn mock()
+ }
+ val messageStoreManager = mock {
+ on { getMessageStore(account) } doReturn mock()
+ }
+
+ return NotificationRepository(
+ notificationStoreProvider,
+ localStoreProvider,
+ messageStoreManager,
+ notificationContentCreator
+ )
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationsTest.java b/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationsTest.java
deleted file mode 100644
index ed0a1d6a07218cd8cd1c668f97464cf5441b2e32..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationsTest.java
+++ /dev/null
@@ -1,377 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.Notification;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.K9.NotificationHideSubject;
-import com.fsck.k9.K9RobolectricTest;
-import com.fsck.k9.controller.MessageReference;
-import com.fsck.k9.mailstore.LocalMessage;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-
-public class NewMailNotificationsTest extends K9RobolectricTest {
- private static final int ACCOUNT_NUMBER = 23;
-
- private Account account;
- private TestNewMailNotifications newMailNotifications;
- private NotificationManagerCompat notificationManager;
- private NotificationContentCreator contentCreator;
- private DeviceNotifications deviceNotifications;
- private WearNotifications wearNotifications;
-
-
- @Before
- public void setUp() throws Exception {
- account = createAccount();
-
- notificationManager = createNotificationManager();
- NotificationHelper notificationHelper = createNotificationHelper(notificationManager);
- contentCreator = createNotificationContentCreator();
- deviceNotifications = createDeviceNotifications();
- wearNotifications = createWearNotifications();
-
- newMailNotifications = new TestNewMailNotifications(notificationHelper, contentCreator, deviceNotifications,
- wearNotifications);
- }
-
- @Test
- public void testAddNewMailNotification() throws Exception {
- int notificationIndex = 0;
- LocalMessage message = createLocalMessage();
- NotificationContent content = createNotificationContent();
- NotificationHolder holder = createNotificationHolder(content, notificationIndex);
- addToNotificationContentCreator(message, content);
- whenAddingContentReturn(content, AddNotificationResult.newNotification(holder));
- Notification wearNotification = createNotification();
- Notification summaryNotification = createNotification();
- addToWearNotifications(holder, wearNotification);
- addToDeviceNotifications(summaryNotification);
-
- newMailNotifications.addNewMailNotification(account, message, 42);
-
- int wearNotificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- int summaryNotificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- verify(notificationManager).notify(wearNotificationId, wearNotification);
- verify(notificationManager).notify(summaryNotificationId, summaryNotification);
- }
-
- @Test
- public void testAddNewMailNotificationWithCancelingExistingNotification() throws Exception {
- int notificationIndex = 0;
- LocalMessage message = createLocalMessage();
- NotificationContent content = createNotificationContent();
- NotificationHolder holder = createNotificationHolder(content, notificationIndex);
- addToNotificationContentCreator(message, content);
- whenAddingContentReturn(content, AddNotificationResult.replaceNotification(holder));
- Notification wearNotification = createNotification();
- Notification summaryNotification = createNotification();
- addToWearNotifications(holder, wearNotification);
- addToDeviceNotifications(summaryNotification);
-
- newMailNotifications.addNewMailNotification(account, message, 42);
-
- int wearNotificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- int summaryNotificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- verify(notificationManager).notify(wearNotificationId, wearNotification);
- verify(notificationManager).cancel(wearNotificationId);
- verify(notificationManager).notify(summaryNotificationId, summaryNotification);
- }
-
- @Test
- public void testAddNewMailNotificationWithPrivacyModeEnabled() throws Exception {
- enablePrivacyMode();
- int notificationIndex = 0;
- LocalMessage message = createLocalMessage();
- NotificationContent content = createNotificationContent();
- NotificationHolder holder = createNotificationHolder(content, notificationIndex);
- addToNotificationContentCreator(message, content);
- whenAddingContentReturn(content, AddNotificationResult.newNotification(holder));
- Notification wearNotification = createNotification();
- addToDeviceNotifications(wearNotification);
-
- newMailNotifications.addNewMailNotification(account, message, 42);
-
- int wearNotificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- int summaryNotificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- verify(notificationManager, never()).notify(eq(wearNotificationId), any(Notification.class));
- verify(notificationManager).notify(summaryNotificationId, wearNotification);
- }
-
- @Test
- public void testAddNewMailNotificationTwice() throws Exception {
- int notificationIndexOne = 0;
- int notificationIndexTwo = 1;
- LocalMessage messageOne = createLocalMessage();
- LocalMessage messageTwo = createLocalMessage();
- NotificationContent contentOne = createNotificationContent();
- NotificationContent contentTwo = createNotificationContent();
- NotificationHolder holderOne = createNotificationHolder(contentOne, notificationIndexOne);
- NotificationHolder holderTwo = createNotificationHolder(contentTwo, notificationIndexTwo);
- addToNotificationContentCreator(messageOne, contentOne);
- addToNotificationContentCreator(messageTwo, contentTwo);
- whenAddingContentReturn(contentOne, AddNotificationResult.newNotification(holderOne));
- whenAddingContentReturn(contentTwo, AddNotificationResult.newNotification(holderTwo));
- Notification wearNotificationOne = createNotification();
- Notification wearNotificationTwo = createNotification();
- Notification summaryNotification = createNotification();
- addToWearNotifications(holderOne, wearNotificationOne);
- addToWearNotifications(holderTwo, wearNotificationTwo);
- addToDeviceNotifications(summaryNotification);
-
- newMailNotifications.addNewMailNotification(account, messageOne, 42);
- newMailNotifications.addNewMailNotification(account, messageTwo, 42);
-
- int wearNotificationIdOne = NotificationIds.getNewMailStackedNotificationId(account, notificationIndexOne);
- int wearNotificationIdTwo = NotificationIds.getNewMailStackedNotificationId(account, notificationIndexTwo);
- int summaryNotificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- verify(notificationManager).notify(wearNotificationIdOne, wearNotificationOne);
- verify(notificationManager).notify(wearNotificationIdTwo, wearNotificationTwo);
- verify(notificationManager, times(2)).notify(summaryNotificationId, summaryNotification);
- }
-
- @Test
- public void testRemoveNewMailNotificationWithoutNotificationData() throws Exception {
- MessageReference messageReference = createMessageReference(1);
-
- newMailNotifications.removeNewMailNotification(account, messageReference);
-
- verify(notificationManager, never()).cancel(anyInt());
- }
-
- @Test
- public void testRemoveNewMailNotificationWithUnknownMessageReference() throws Exception {
- enablePrivacyMode();
- MessageReference messageReference = createMessageReference(1);
- int notificationIndex = 0;
- LocalMessage message = createLocalMessage();
- NotificationContent content = createNotificationContent();
- NotificationHolder holder = createNotificationHolder(content, notificationIndex);
- addToNotificationContentCreator(message, content);
- whenAddingContentReturn(content, AddNotificationResult.newNotification(holder));
- Notification summaryNotification = createNotification();
- addToDeviceNotifications(summaryNotification);
- newMailNotifications.addNewMailNotification(account, message, 23);
- whenRemovingContentReturn(messageReference, RemoveNotificationResult.unknownNotification());
-
- newMailNotifications.removeNewMailNotification(account, messageReference);
-
- verify(notificationManager, never()).cancel(anyInt());
- }
-
- @Test
- public void testRemoveNewMailNotification() throws Exception {
- enablePrivacyMode();
- MessageReference messageReference = createMessageReference(1);
- int notificationIndex = 0;
- int notificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- LocalMessage message = createLocalMessage();
- NotificationContent content = createNotificationContent();
- NotificationHolder holder = createNotificationHolder(content, notificationIndex);
- addToNotificationContentCreator(message, content);
- whenAddingContentReturn(content, AddNotificationResult.newNotification(holder));
- Notification summaryNotification = createNotification();
- addToDeviceNotifications(summaryNotification);
- newMailNotifications.addNewMailNotification(account, message, 23);
- whenRemovingContentReturn(messageReference, RemoveNotificationResult.cancelNotification(notificationId));
-
- newMailNotifications.removeNewMailNotification(account, messageReference);
-
- int summaryNotificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- verify(notificationManager).cancel(notificationId);
- verify(notificationManager, times(2)).notify(summaryNotificationId, summaryNotification);
- }
-
- @Test
- public void testRemoveNewMailNotificationClearingAllNotifications() throws Exception {
- MessageReference messageReference = createMessageReference(1);
- int notificationIndex = 0;
- int notificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- LocalMessage message = createLocalMessage();
- NotificationContent content = createNotificationContent();
- NotificationHolder holder = createNotificationHolder(content, notificationIndex);
- addToNotificationContentCreator(message, content);
- whenAddingContentReturn(content, AddNotificationResult.newNotification(holder));
- Notification summaryNotification = createNotification();
- addToDeviceNotifications(summaryNotification);
- newMailNotifications.addNewMailNotification(account, message, 23);
- whenRemovingContentReturn(messageReference, RemoveNotificationResult.cancelNotification(notificationId));
- when(newMailNotifications.notificationData.getNewMessagesCount()).thenReturn(0);
- setActiveNotificationIds();
-
- newMailNotifications.removeNewMailNotification(account, messageReference);
-
- int summaryNotificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- verify(notificationManager).cancel(notificationId);
- verify(notificationManager).cancel(summaryNotificationId);
- }
-
- @Test
- public void testRemoveNewMailNotificationWithCreateNotification() throws Exception {
- MessageReference messageReference = createMessageReference(1);
- int notificationIndex = 0;
- int notificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- LocalMessage message = createLocalMessage();
- NotificationContent contentOne = createNotificationContent();
- NotificationContent contentTwo = createNotificationContent();
- NotificationHolder holderOne = createNotificationHolder(contentOne, notificationIndex);
- NotificationHolder holderTwo = createNotificationHolder(contentTwo, notificationIndex);
- addToNotificationContentCreator(message, contentOne);
- whenAddingContentReturn(contentOne, AddNotificationResult.newNotification(holderOne));
- Notification summaryNotification = createNotification();
- addToDeviceNotifications(summaryNotification);
- Notification wearNotificationOne = createNotification();
- Notification wearNotificationTwo = createNotification();
- addToWearNotifications(holderOne, wearNotificationOne);
- addToWearNotifications(holderTwo, wearNotificationTwo);
- newMailNotifications.addNewMailNotification(account, message, 23);
- whenRemovingContentReturn(messageReference, RemoveNotificationResult.createNotification(holderTwo));
-
- newMailNotifications.removeNewMailNotification(account, messageReference);
-
- int summaryNotificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- verify(notificationManager).cancel(notificationId);
- verify(notificationManager).notify(notificationId, wearNotificationTwo);
- verify(notificationManager, times(2)).notify(summaryNotificationId, summaryNotification);
- }
-
- @Test
- public void testClearNewMailNotificationsWithoutNotificationData() throws Exception {
- newMailNotifications.clearNewMailNotifications(account);
-
- verify(notificationManager, never()).cancel(anyInt());
- }
-
- @Test
- public void testClearNewMailNotifications() throws Exception {
- int notificationIndex = 0;
- int notificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- LocalMessage message = createLocalMessage();
- NotificationContent content = createNotificationContent();
- NotificationHolder holder = createNotificationHolder(content, notificationIndex);
- addToNotificationContentCreator(message, content);
- setActiveNotificationIds(notificationId);
- whenAddingContentReturn(content, AddNotificationResult.newNotification(holder));
- newMailNotifications.addNewMailNotification(account, message, 3);
-
- newMailNotifications.clearNewMailNotifications(account);
-
- verify(notificationManager).cancel(notificationId);
- verify(notificationManager).cancel(NotificationIds.getNewMailSummaryNotificationId(account));
- }
-
- private Account createAccount() {
- Account account = mock(Account.class);
- when(account.getAccountNumber()).thenReturn(ACCOUNT_NUMBER);
- return account;
- }
-
- private LocalMessage createLocalMessage() {
- return mock(LocalMessage.class);
- }
-
- private NotificationContent createNotificationContent() {
- return new NotificationContent(null, null, null, null, null, false);
- }
-
- private NotificationHolder createNotificationHolder(NotificationContent content, int index) {
- int notificationId = NotificationIds.getNewMailStackedNotificationId(account, index);
- return new NotificationHolder(notificationId, content);
- }
-
- private NotificationManagerCompat createNotificationManager() {
- return mock(NotificationManagerCompat.class);
- }
-
- private NotificationHelper createNotificationHelper(NotificationManagerCompat notificationManager) {
- NotificationHelper notificationHelper = mock(NotificationHelper.class);
- when(notificationHelper.getNotificationManager()).thenReturn(notificationManager);
- return notificationHelper;
- }
-
- private NotificationContentCreator createNotificationContentCreator() {
- return mock(NotificationContentCreator.class);
- }
-
- private void addToNotificationContentCreator(LocalMessage message, NotificationContent content) {
- when(contentCreator.createFromMessage(account, message)).thenReturn(content);
- }
-
- private DeviceNotifications createDeviceNotifications() {
- return mock(DeviceNotifications.class);
- }
-
- private void addToDeviceNotifications(Notification notificationToReturn) {
- when(deviceNotifications.buildSummaryNotification(
- eq(account), eq(newMailNotifications.notificationData), anyBoolean())
- ).thenReturn(notificationToReturn);
- }
-
- private Notification createNotification() {
- return mock(Notification.class);
- }
-
- private WearNotifications createWearNotifications() {
- return mock(WearNotifications.class);
- }
-
- private MessageReference createMessageReference(int number) {
- return new MessageReference("account", 1, String.valueOf(number), null);
- }
-
- private void addToWearNotifications(NotificationHolder notificationHolder, Notification notificationToReturn) {
- when(wearNotifications.buildStackedNotification(account, notificationHolder)).thenReturn(notificationToReturn);
- }
-
- private void whenAddingContentReturn(NotificationContent content, AddNotificationResult result) {
- NotificationData notificationData = newMailNotifications.notificationData;
- when(notificationData.addNotificationContent(content)).thenReturn(result);
-
- int newCount = notificationData.getNewMessagesCount() + 1;
- when(notificationData.getNewMessagesCount()).thenReturn(newCount);
- }
-
- private void whenRemovingContentReturn(MessageReference messageReference, RemoveNotificationResult result) {
- NotificationData notificationData = newMailNotifications.notificationData;
- when(notificationData.removeNotificationForMessage(messageReference)).thenReturn(result);
- }
-
- private void setActiveNotificationIds(int... notificationIds) {
- NotificationData notificationData = newMailNotifications.notificationData;
- when(notificationData.getActiveNotificationIds()).thenReturn(notificationIds);
- }
-
- private void enablePrivacyMode() {
- K9.setNotificationHideSubject(NotificationHideSubject.ALWAYS);
- }
-
- static class TestNewMailNotifications extends NewMailNotifications {
-
- public final NotificationData notificationData;
-
- TestNewMailNotifications(NotificationHelper notificationHelper, NotificationContentCreator contentCreator,
- DeviceNotifications deviceNotifications, WearNotifications wearNotifications) {
- super(notificationHelper, contentCreator, deviceNotifications, wearNotifications);
- notificationData = mock(NotificationData.class);
- }
-
- @Override
- NotificationData createNotificationData(Account account, int unreadMessageCount) {
- return notificationData;
- }
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/NotificationContentCreatorTest.java b/app/core/src/test/java/com/fsck/k9/notification/NotificationContentCreatorTest.java
deleted file mode 100644
index eeb1b230d5785d003e4a7b1ef3d7076d1766d787..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/NotificationContentCreatorTest.java
+++ /dev/null
@@ -1,181 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.content.Context;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.RobolectricTest;
-import com.fsck.k9.controller.MessageReference;
-import com.fsck.k9.mail.Address;
-import com.fsck.k9.mail.Flag;
-import com.fsck.k9.mail.Message.RecipientType;
-import com.fsck.k9.mailstore.LocalMessage;
-import com.fsck.k9.message.extractors.PreviewResult.PreviewType;
-import org.junit.Before;
-import org.junit.Test;
-import org.robolectric.RuntimeEnvironment;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-
-public class NotificationContentCreatorTest extends RobolectricTest {
- private static final String ACCOUNT_UUID = "1-2-3";
- private static final long FOLDER_ID = 23;
- private static final String FOLDER_NAME = "INBOX";
- private static final String UID = "42";
- private static final String PREVIEW = "Message preview text";
- private static final String SUBJECT = "Message subject";
- private static final String SENDER_ADDRESS = "alice@example.com";
- private static final String SENDER_NAME = "Alice";
- private static final String RECIPIENT_ADDRESS = "bob@example.com";
- private static final String RECIPIENT_NAME = "Bob";
-
-
- private NotificationResourceProvider resourceProvider = new TestNotificationResourceProvider();
- private NotificationContentCreator contentCreator;
- private MessageReference messageReference;
- private Account account;
- private LocalMessage message;
-
-
- @Before
- public void setUp() throws Exception {
- contentCreator = createNotificationContentCreator();
- messageReference = createMessageReference();
- account = createFakeAccount();
- message = createFakeLocalMessage(messageReference);
- }
-
- @Test
- public void createFromMessage_withRegularMessage() throws Exception {
- NotificationContent content = contentCreator.createFromMessage(account, message);
-
- assertEquals(messageReference, content.messageReference);
- assertEquals(SENDER_NAME, content.sender);
- assertEquals(SUBJECT, content.subject);
- assertEquals(SUBJECT + "\n" + PREVIEW, content.preview.toString());
- assertEquals(SENDER_NAME + " " + SUBJECT, content.summary.toString());
- assertEquals(false, content.starred);
- }
-
- @Test
- public void createFromMessage_withoutSubject() throws Exception {
- when(message.getSubject()).thenReturn(null);
-
- NotificationContent content = contentCreator.createFromMessage(account, message);
-
- String noSubject = "(No subject)";
- assertEquals(noSubject, content.subject);
- assertEquals(PREVIEW, content.preview.toString());
- assertEquals(SENDER_NAME + " " + noSubject, content.summary.toString());
- }
-
- @Test
- public void createFromMessage_withoutPreview() throws Exception {
- when(message.getPreviewType()).thenReturn(PreviewType.NONE);
- when(message.getPreview()).thenReturn(null);
-
- NotificationContent content = contentCreator.createFromMessage(account, message);
-
- assertEquals(SUBJECT, content.subject);
- assertEquals(SUBJECT, content.preview.toString());
- }
-
- @Test
- public void createFromMessage_withErrorPreview() throws Exception {
- when(message.getPreviewType()).thenReturn(PreviewType.ERROR);
- when(message.getPreview()).thenReturn(null);
-
- NotificationContent content = contentCreator.createFromMessage(account, message);
-
- assertEquals(SUBJECT, content.subject);
- assertEquals(SUBJECT, content.preview.toString());
- }
-
- @Test
- public void createFromMessage_withEncryptedMessage() throws Exception {
- when(message.getPreviewType()).thenReturn(PreviewType.ENCRYPTED);
- when(message.getPreview()).thenReturn(null);
-
- NotificationContent content = contentCreator.createFromMessage(account, message);
-
- String encrypted = "*Encrypted*";
- assertEquals(SUBJECT, content.subject);
- assertEquals(SUBJECT + "\n" + encrypted, content.preview.toString());
- }
-
- @Test
- public void createFromMessage_withoutSender() throws Exception {
- when(message.getFrom()).thenReturn(null);
-
- NotificationContent content = contentCreator.createFromMessage(account, message);
-
- assertEquals("No sender", content.sender);
- assertEquals(SUBJECT, content.summary.toString());
- }
-
- @Test
- public void createFromMessage_withMessageFromSelf() throws Exception {
- when(account.isAnIdentity(any(Address[].class))).thenReturn(true);
-
- NotificationContent content = contentCreator.createFromMessage(account, message);
-
- String insteadOfSender = "To:Bob";
- assertEquals(insteadOfSender, content.sender);
- assertEquals(insteadOfSender + " " + SUBJECT, content.summary.toString());
- }
-
- @Test
- public void createFromMessage_withStarredMessage() throws Exception {
- when(message.isSet(Flag.FLAGGED)).thenReturn(true);
-
- NotificationContent content = contentCreator.createFromMessage(account, message);
-
- assertEquals(true, content.starred);
- }
-
- @Test
- public void createFromMessage_withoutEmptyMessage() throws Exception {
- when(message.getFrom()).thenReturn(null);
- when(message.getSubject()).thenReturn(null);
- when(message.getPreviewType()).thenReturn(PreviewType.NONE);
- when(message.getPreview()).thenReturn(null);
-
- NotificationContent content = contentCreator.createFromMessage(account, message);
-
- assertEquals("No sender", content.sender);
- assertEquals("(No subject)", content.subject);
- assertEquals("(No subject)", content.preview.toString());
- assertEquals("(No subject)", content.summary.toString());
- }
-
- private NotificationContentCreator createNotificationContentCreator() {
- Context context = RuntimeEnvironment.application;
- return new NotificationContentCreator(context, resourceProvider);
- }
-
- private Account createFakeAccount() {
- return mock(Account.class);
- }
-
- private MessageReference createMessageReference() {
- return new MessageReference(ACCOUNT_UUID, FOLDER_ID, UID, null);
- }
-
- private LocalMessage createFakeLocalMessage(MessageReference messageReference) {
- LocalMessage message = mock(LocalMessage.class);
-
- when(message.makeMessageReference()).thenReturn(messageReference);
- when(message.getPreviewType()).thenReturn(PreviewType.TEXT);
- when(message.getPreview()).thenReturn(PREVIEW);
- when(message.getSubject()).thenReturn(SUBJECT);
- when(message.getFrom()).thenReturn(new Address[] { new Address(SENDER_ADDRESS, SENDER_NAME) });
- when(message.getRecipients(RecipientType.TO))
- .thenReturn(new Address[] { new Address(RECIPIENT_ADDRESS, RECIPIENT_NAME) });
-
- return message;
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/NotificationContentCreatorTest.kt b/app/core/src/test/java/com/fsck/k9/notification/NotificationContentCreatorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c706443137b72cee6e002e7857d4b386697c7ebc
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/NotificationContentCreatorTest.kt
@@ -0,0 +1,159 @@
+package com.fsck.k9.notification
+
+import androidx.test.core.app.ApplicationProvider
+import com.fsck.k9.Account
+import com.fsck.k9.RobolectricTest
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.mail.Address
+import com.fsck.k9.mail.Message.RecipientType
+import com.fsck.k9.mailstore.LocalMessage
+import com.fsck.k9.message.extractors.PreviewResult.PreviewType
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stubbing
+
+private const val ACCOUNT_UUID = "1-2-3"
+private const val FOLDER_ID = 23L
+private const val UID = "42"
+private const val PREVIEW = "Message preview text"
+private const val SUBJECT = "Message subject"
+private const val SENDER_ADDRESS = "alice@example.com"
+private const val SENDER_NAME = "Alice"
+private const val RECIPIENT_ADDRESS = "bob@example.com"
+private const val RECIPIENT_NAME = "Bob"
+
+class NotificationContentCreatorTest : RobolectricTest() {
+ private val resourceProvider = TestNotificationResourceProvider()
+ private val contentCreator = createNotificationContentCreator()
+ private val messageReference = createMessageReference()
+ private val account = createFakeAccount()
+ private val message = createFakeLocalMessage(messageReference)
+
+ @Test
+ fun createFromMessage_withRegularMessage() {
+ val content = contentCreator.createFromMessage(account, message)
+
+ assertThat(content.messageReference).isEqualTo(messageReference)
+ assertThat(content.sender).isEqualTo(SENDER_NAME)
+ assertThat(content.subject).isEqualTo(SUBJECT)
+ assertThat(content.preview.toString()).isEqualTo("$SUBJECT\n$PREVIEW")
+ assertThat(content.summary.toString()).isEqualTo("$SENDER_NAME $SUBJECT")
+ }
+
+ @Test
+ fun createFromMessage_withoutSubject() {
+ stubbing(message) {
+ on { subject } doReturn null
+ }
+
+ val content = contentCreator.createFromMessage(account, message)
+
+ assertThat(content.subject).isEqualTo("(No subject)")
+ assertThat(content.preview.toString()).isEqualTo(PREVIEW)
+ assertThat(content.summary.toString()).isEqualTo("$SENDER_NAME (No subject)")
+ }
+
+ @Test
+ fun createFromMessage_withoutPreview() {
+ stubbing(message) {
+ on { previewType } doReturn PreviewType.NONE
+ on { preview } doReturn null
+ }
+
+ val content = contentCreator.createFromMessage(account, message)
+
+ assertThat(content.subject).isEqualTo(SUBJECT)
+ assertThat(content.preview.toString()).isEqualTo(SUBJECT)
+ }
+
+ @Test
+ fun createFromMessage_withErrorPreview() {
+ stubbing(message) {
+ on { previewType } doReturn PreviewType.ERROR
+ on { preview } doReturn null
+ }
+
+ val content = contentCreator.createFromMessage(account, message)
+
+ assertThat(content.subject).isEqualTo(SUBJECT)
+ assertThat(content.preview.toString()).isEqualTo(SUBJECT)
+ }
+
+ @Test
+ fun createFromMessage_withEncryptedMessage() {
+ stubbing(message) {
+ on { previewType } doReturn PreviewType.ENCRYPTED
+ on { preview } doReturn null
+ }
+
+ val content = contentCreator.createFromMessage(account, message)
+
+ assertThat(content.subject).isEqualTo(SUBJECT)
+ assertThat(content.preview.toString()).isEqualTo("$SUBJECT\n*Encrypted*")
+ }
+
+ @Test
+ fun createFromMessage_withoutSender() {
+ stubbing(message) {
+ on { from } doReturn null
+ }
+
+ val content = contentCreator.createFromMessage(account, message)
+
+ assertThat(content.sender).isEqualTo("No sender")
+ assertThat(content.summary.toString()).isEqualTo(SUBJECT)
+ }
+
+ @Test
+ fun createFromMessage_withMessageFromSelf() {
+ stubbing(account) {
+ on { isAnIdentity(any>()) } doReturn true
+ }
+
+ val content = contentCreator.createFromMessage(account, message)
+
+ assertThat(content.sender).isEqualTo("To:Bob")
+ assertThat(content.summary.toString()).isEqualTo("To:Bob $SUBJECT")
+ }
+
+ @Test
+ fun createFromMessage_withoutEmptyMessage() {
+ stubbing(message) {
+ on { from } doReturn null
+ on { subject } doReturn null
+ on { previewType } doReturn PreviewType.NONE
+ on { preview } doReturn null
+ }
+
+ val content = contentCreator.createFromMessage(account, message)
+
+ assertThat(content.sender).isEqualTo("No sender")
+ assertThat(content.subject).isEqualTo("(No subject)")
+ assertThat(content.preview.toString()).isEqualTo("(No subject)")
+ assertThat(content.summary.toString()).isEqualTo("(No subject)")
+ }
+
+ private fun createNotificationContentCreator(): NotificationContentCreator {
+ return NotificationContentCreator(ApplicationProvider.getApplicationContext(), resourceProvider)
+ }
+
+ private fun createFakeAccount(): Account = mock()
+
+ private fun createMessageReference(): MessageReference {
+ return MessageReference(ACCOUNT_UUID, FOLDER_ID, UID)
+ }
+
+ private fun createFakeLocalMessage(messageReference: MessageReference): LocalMessage {
+ return mock {
+ on { makeMessageReference() } doReturn messageReference
+ on { previewType } doReturn PreviewType.TEXT
+ on { preview } doReturn PREVIEW
+ on { subject } doReturn SUBJECT
+ on { from } doReturn arrayOf(Address(SENDER_ADDRESS, SENDER_NAME))
+ on { getRecipients(RecipientType.TO) } doReturn arrayOf(Address(RECIPIENT_ADDRESS, RECIPIENT_NAME))
+ }
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..9bb2c110bb2fd919c5008145a5014d6d75668fb9
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt
@@ -0,0 +1,154 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.RobolectricTest
+import com.fsck.k9.controller.MessageReference
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
+import org.junit.Test
+
+private const val ACCOUNT_UUID = "1-2-3"
+private const val ACCOUNT_NUMBER = 23
+private const val FOLDER_ID = 42L
+private const val TIMESTAMP = 0L
+
+class NotificationDataStoreTest : RobolectricTest() {
+ private val account = createAccount()
+ private val notificationDataStore = NotificationDataStore()
+
+ @Test
+ fun testAddNotificationContent() {
+ val content = createNotificationContent("1")
+
+ val result = notificationDataStore.addNotification(account, content, TIMESTAMP)
+
+ assertThat(result.shouldCancelNotification).isFalse()
+
+ val holder = result.notificationHolder
+
+ assertThat(holder).isNotNull()
+ assertThat(holder.notificationId).isEqualTo(NotificationIds.getSingleMessageNotificationId(account, 0))
+ assertThat(holder.content).isEqualTo(content)
+ }
+
+ @Test
+ fun testAddNotificationContentWithReplacingNotification() {
+ 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 result = notificationDataStore.addNotification(account, createNotificationContent("9"), TIMESTAMP)
+
+ assertThat(result.shouldCancelNotification).isTrue()
+ assertThat(result.cancelNotificationId).isEqualTo(NotificationIds.getSingleMessageNotificationId(account, 0))
+ }
+
+ @Test
+ fun testRemoveNotificationForMessage() {
+ val content = createNotificationContent("1")
+ notificationDataStore.addNotification(account, content, TIMESTAMP)
+
+ val result = notificationDataStore.removeNotifications(account) { listOf(content.messageReference) }
+
+ assertNotNull(result) { removeResult ->
+ assertThat(removeResult.cancelNotificationIds)
+ .containsExactly(NotificationIds.getSingleMessageNotificationId(account, 0))
+ assertThat(removeResult.notificationHolders).isEmpty()
+ }
+ }
+
+ @Test
+ fun testRemoveNotificationForMessageWithRecreatingNotification() {
+ notificationDataStore.addNotification(account, createNotificationContent("1"), TIMESTAMP)
+ val content = createNotificationContent("2")
+ notificationDataStore.addNotification(account, content, 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)
+ notificationDataStore.addNotification(account, createNotificationContent("9"), TIMESTAMP)
+ val latestContent = createNotificationContent("10")
+ notificationDataStore.addNotification(account, latestContent, TIMESTAMP)
+
+ val result = notificationDataStore.removeNotifications(account) { listOf(latestContent.messageReference) }
+
+ assertNotNull(result) { removeResult ->
+ assertThat(removeResult.cancelNotificationIds)
+ .containsExactly(NotificationIds.getSingleMessageNotificationId(account, 1))
+ assertThat(removeResult.notificationHolders).hasSize(1)
+
+ val holder = removeResult.notificationHolders.first()
+ assertThat(holder.notificationId).isEqualTo(NotificationIds.getSingleMessageNotificationId(account, 1))
+ assertThat(holder.content).isEqualTo(content)
+ }
+ }
+
+ @Test
+ fun testRemoveDoesNotLeakNotificationIds() {
+ for (i in 1..MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS + 1) {
+ val content = createNotificationContent(i.toString())
+ notificationDataStore.addNotification(account, content, TIMESTAMP)
+ notificationDataStore.removeNotifications(account) { listOf(content.messageReference) }
+ }
+ }
+
+ @Test
+ fun testNewMessagesCount() {
+ val contentOne = createNotificationContent("1")
+ val resultOne = notificationDataStore.addNotification(account, contentOne, TIMESTAMP)
+ assertThat(resultOne.notificationData.newMessagesCount).isEqualTo(1)
+
+ val contentTwo = createNotificationContent("2")
+ val resultTwo = notificationDataStore.addNotification(account, contentTwo, TIMESTAMP)
+ assertThat(resultTwo.notificationData.newMessagesCount).isEqualTo(2)
+ }
+
+ @Test
+ fun testIsSingleMessageNotification() {
+ val resultOne = notificationDataStore.addNotification(account, createNotificationContent("1"), TIMESTAMP)
+ assertThat(resultOne.notificationData.isSingleMessageNotification).isTrue()
+
+ val resultTwo = notificationDataStore.addNotification(account, createNotificationContent("2"), TIMESTAMP)
+ assertThat(resultTwo.notificationData.isSingleMessageNotification).isFalse()
+ }
+
+ @Test
+ fun testGetHolderForLatestNotification() {
+ val content = createNotificationContent("1")
+ val addResult = notificationDataStore.addNotification(account, content, TIMESTAMP)
+
+ assertThat(addResult.notificationData.activeNotifications.first()).isEqualTo(addResult.notificationHolder)
+ }
+
+ private fun createAccount(): Account {
+ return Account("00000000-0000-4000-0000-000000000000").apply {
+ accountNumber = ACCOUNT_NUMBER
+ }
+ }
+
+ private fun createMessageReference(uid: String): MessageReference {
+ return MessageReference(ACCOUNT_UUID, FOLDER_ID, uid)
+ }
+
+ private fun createNotificationContent(uid: String): NotificationContent {
+ val messageReference = createMessageReference(uid)
+ return createNotificationContent(messageReference)
+ }
+
+ private fun createNotificationContent(messageReference: MessageReference): NotificationContent {
+ return NotificationContent(
+ messageReference = messageReference,
+ sender = "irrelevant",
+ subject = "irrelevant",
+ preview = "irrelevant",
+ summary = "irrelevant"
+ )
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/NotificationDataTest.java b/app/core/src/test/java/com/fsck/k9/notification/NotificationDataTest.java
deleted file mode 100644
index 9fc5456eb15d4989fccccd29db46d03a78baab27..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/NotificationDataTest.java
+++ /dev/null
@@ -1,315 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.List;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.RobolectricTest;
-import com.fsck.k9.controller.MessageReference;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-
-public class NotificationDataTest extends RobolectricTest {
- private static final String ACCOUNT_UUID = "1-2-3";
- private static final int ACCOUNT_NUMBER = 23;
- private static final long FOLDER_ID = 42;
- private static final String FOLDER_NAME = "INBOX";
-
-
- private NotificationData notificationData;
- private Account account;
-
-
- @Before
- public void setUp() throws Exception {
- account = createFakeAccount();
- notificationData = new NotificationData(account);
- }
-
- @Test
- public void testAddNotificationContent() throws Exception {
- NotificationContent content = createNotificationContent("1");
-
- AddNotificationResult result = notificationData.addNotificationContent(content);
-
- assertFalse(result.shouldCancelNotification());
- NotificationHolder holder = result.getNotificationHolder();
- assertNotNull(holder);
- assertEquals(NotificationIds.getNewMailStackedNotificationId(account, 0), holder.notificationId);
- assertEquals(content, holder.content);
- }
-
- @Test
- public void testAddNotificationContentWithReplacingNotification() throws Exception {
- notificationData.addNotificationContent(createNotificationContent("1"));
- notificationData.addNotificationContent(createNotificationContent("2"));
- notificationData.addNotificationContent(createNotificationContent("3"));
- notificationData.addNotificationContent(createNotificationContent("4"));
- notificationData.addNotificationContent(createNotificationContent("5"));
- notificationData.addNotificationContent(createNotificationContent("6"));
- notificationData.addNotificationContent(createNotificationContent("7"));
- notificationData.addNotificationContent(createNotificationContent("8"));
-
- AddNotificationResult result = notificationData.addNotificationContent(createNotificationContent("9"));
-
- assertTrue(result.shouldCancelNotification());
- assertEquals(NotificationIds.getNewMailStackedNotificationId(account, 0), result.getNotificationId());
- }
-
- @Test
- public void testRemoveNotificationForMessage() throws Exception {
- NotificationContent content = createNotificationContent("1");
- notificationData.addNotificationContent(content);
-
- RemoveNotificationResult result = notificationData.removeNotificationForMessage(content.messageReference);
-
- assertFalse(result.isUnknownNotification());
- assertEquals(NotificationIds.getNewMailStackedNotificationId(account, 0), result.getNotificationId());
- assertFalse(result.shouldCreateNotification());
- }
-
- @Test
- public void testRemoveNotificationForMessageWithRecreatingNotification() throws Exception {
- notificationData.addNotificationContent(createNotificationContent("1"));
- NotificationContent content = createNotificationContent("2");
- notificationData.addNotificationContent(content);
- notificationData.addNotificationContent(createNotificationContent("3"));
- notificationData.addNotificationContent(createNotificationContent("4"));
- notificationData.addNotificationContent(createNotificationContent("5"));
- notificationData.addNotificationContent(createNotificationContent("6"));
- notificationData.addNotificationContent(createNotificationContent("7"));
- notificationData.addNotificationContent(createNotificationContent("8"));
- notificationData.addNotificationContent(createNotificationContent("9"));
- NotificationContent latestContent = createNotificationContent("10");
- notificationData.addNotificationContent(latestContent);
-
- RemoveNotificationResult result =
- notificationData.removeNotificationForMessage(latestContent.messageReference);
-
- assertFalse(result.isUnknownNotification());
- assertEquals(NotificationIds.getNewMailStackedNotificationId(account, 1), result.getNotificationId());
- assertTrue(result.shouldCreateNotification());
- NotificationHolder holder = result.getNotificationHolder();
- assertNotNull(holder);
- assertEquals(NotificationIds.getNewMailStackedNotificationId(account, 1), holder.notificationId);
- assertEquals(content, holder.content);
- }
-
- @Test
- public void testRemoveDoesNotLeakNotificationIds() {
- for (int i = 1; i <= NotificationData.MAX_NUMBER_OF_STACKED_NOTIFICATIONS + 1; i++) {
- NotificationContent content = createNotificationContent("" + i);
- notificationData.addNotificationContent(content);
- notificationData.removeNotificationForMessage(content.messageReference);
- }
- }
-
- @Test
- public void testNewMessagesCount() throws Exception {
- assertEquals(0, notificationData.getNewMessagesCount());
-
- NotificationContent contentOne = createNotificationContent("1");
- notificationData.addNotificationContent(contentOne);
- assertEquals(1, notificationData.getNewMessagesCount());
-
- NotificationContent contentTwo = createNotificationContent("2");
- notificationData.addNotificationContent(contentTwo);
- assertEquals(2, notificationData.getNewMessagesCount());
- }
-
- @Test
- public void testUnreadMessagesCount() throws Exception {
- notificationData.setUnreadMessageCount(42);
- assertEquals(42, notificationData.getUnreadMessageCount());
-
- NotificationContent content = createNotificationContent("1");
- notificationData.addNotificationContent(content);
- assertEquals(43, notificationData.getUnreadMessageCount());
-
- NotificationContent contentTwo = createNotificationContent("2");
- notificationData.addNotificationContent(contentTwo);
- assertEquals(44, notificationData.getUnreadMessageCount());
- }
-
- @Test
- public void testContainsStarredMessages() throws Exception {
- assertFalse(notificationData.containsStarredMessages());
-
- notificationData.addNotificationContent(createNotificationContentForStarredMessage());
-
- assertTrue(notificationData.containsStarredMessages());
- }
-
- @Test
- public void testContainsStarredMessagesWithAdditionalMessages() throws Exception {
- notificationData.addNotificationContent(createNotificationContent("1"));
- notificationData.addNotificationContent(createNotificationContent("2"));
- notificationData.addNotificationContent(createNotificationContent("3"));
- notificationData.addNotificationContent(createNotificationContent("4"));
- notificationData.addNotificationContent(createNotificationContent("5"));
- notificationData.addNotificationContent(createNotificationContent("6"));
- notificationData.addNotificationContent(createNotificationContent("7"));
- notificationData.addNotificationContent(createNotificationContent("8"));
-
- assertFalse(notificationData.containsStarredMessages());
-
- notificationData.addNotificationContent(createNotificationContentForStarredMessage());
-
- assertTrue(notificationData.containsStarredMessages());
- }
-
- @Test
- public void testIsSingleMessageNotification() throws Exception {
- assertFalse(notificationData.isSingleMessageNotification());
-
- notificationData.addNotificationContent(createNotificationContent("1"));
- assertTrue(notificationData.isSingleMessageNotification());
-
- notificationData.addNotificationContent(createNotificationContent("2"));
- assertFalse(notificationData.isSingleMessageNotification());
- }
-
- @Test
- public void testGetHolderForLatestNotification() throws Exception {
- NotificationContent content = createNotificationContent("1");
- AddNotificationResult addResult = notificationData.addNotificationContent(content);
-
- NotificationHolder holder = notificationData.getHolderForLatestNotification();
-
- assertEquals(addResult.getNotificationHolder(), holder);
- }
-
- @Test
- public void testGetContentForSummaryNotification() throws Exception {
- notificationData.addNotificationContent(createNotificationContent("1"));
- NotificationContent content4 = createNotificationContent("2");
- notificationData.addNotificationContent(content4);
- NotificationContent content3 = createNotificationContent("3");
- notificationData.addNotificationContent(content3);
- NotificationContent content2 = createNotificationContent("4");
- notificationData.addNotificationContent(content2);
- NotificationContent content1 = createNotificationContent("5");
- notificationData.addNotificationContent(content1);
- NotificationContent content0 = createNotificationContent("6");
- notificationData.addNotificationContent(content0);
-
- List contents = notificationData.getContentForSummaryNotification();
-
- assertEquals(5, contents.size());
- assertEquals(content0, contents.get(0));
- assertEquals(content1, contents.get(1));
- assertEquals(content2, contents.get(2));
- assertEquals(content3, contents.get(3));
- assertEquals(content4, contents.get(4));
- }
-
- @Test
- public void testGetActiveNotificationIds() throws Exception {
- notificationData.addNotificationContent(createNotificationContent("1"));
- notificationData.addNotificationContent(createNotificationContent("2"));
-
- int[] notificationIds = notificationData.getActiveNotificationIds();
-
- assertEquals(2, notificationIds.length);
- assertEquals(NotificationIds.getNewMailStackedNotificationId(account, 1), notificationIds[0]);
- assertEquals(NotificationIds.getNewMailStackedNotificationId(account, 0), notificationIds[1]);
- }
-
- @Test
- public void testGetAccount() throws Exception {
- assertEquals(account, notificationData.getAccount());
- }
-
- @Test
- public void testGetAllMessageReferences() throws Exception {
- MessageReference messageReference0 = createMessageReference("1");
- MessageReference messageReference1 = createMessageReference("2");
- MessageReference messageReference2 = createMessageReference("3");
- MessageReference messageReference3 = createMessageReference("4");
- MessageReference messageReference4 = createMessageReference("5");
- MessageReference messageReference5 = createMessageReference("6");
- MessageReference messageReference6 = createMessageReference("7");
- MessageReference messageReference7 = createMessageReference("8");
- MessageReference messageReference8 = createMessageReference("9");
- notificationData.addNotificationContent(createNotificationContent(messageReference8));
- notificationData.addNotificationContent(createNotificationContent(messageReference7));
- notificationData.addNotificationContent(createNotificationContent(messageReference6));
- notificationData.addNotificationContent(createNotificationContent(messageReference5));
- notificationData.addNotificationContent(createNotificationContent(messageReference4));
- notificationData.addNotificationContent(createNotificationContent(messageReference3));
- notificationData.addNotificationContent(createNotificationContent(messageReference2));
- notificationData.addNotificationContent(createNotificationContent(messageReference1));
- notificationData.addNotificationContent(createNotificationContent(messageReference0));
-
- List messageReferences = notificationData.getAllMessageReferences();
-
- assertEquals(9, messageReferences.size());
- assertEquals(messageReference0, messageReferences.get(0));
- assertEquals(messageReference1, messageReferences.get(1));
- assertEquals(messageReference2, messageReferences.get(2));
- assertEquals(messageReference3, messageReferences.get(3));
- assertEquals(messageReference4, messageReferences.get(4));
- assertEquals(messageReference5, messageReferences.get(5));
- assertEquals(messageReference6, messageReferences.get(6));
- assertEquals(messageReference7, messageReferences.get(7));
- assertEquals(messageReference8, messageReferences.get(8));
- }
-
- @Test
- public void testOverflowNotifications() {
- MessageReference messageReference0 = createMessageReference("1");
- MessageReference messageReference1 = createMessageReference("2");
- MessageReference messageReference2 = createMessageReference("3");
- MessageReference messageReference3 = createMessageReference("4");
- MessageReference messageReference4 = createMessageReference("5");
- MessageReference messageReference5 = createMessageReference("6");
- MessageReference messageReference6 = createMessageReference("7");
- MessageReference messageReference7 = createMessageReference("8");
- MessageReference messageReference8 = createMessageReference("9");
-
- notificationData.addNotificationContent(createNotificationContent(messageReference8));
- notificationData.addNotificationContent(createNotificationContent(messageReference7));
- notificationData.addNotificationContent(createNotificationContent(messageReference6));
- notificationData.addNotificationContent(createNotificationContent(messageReference5));
- notificationData.addNotificationContent(createNotificationContent(messageReference4));
- notificationData.addNotificationContent(createNotificationContent(messageReference3));
- notificationData.addNotificationContent(createNotificationContent(messageReference2));
- notificationData.addNotificationContent(createNotificationContent(messageReference1));
- notificationData.addNotificationContent(createNotificationContent(messageReference0));
-
- assertTrue(notificationData.hasSummaryOverflowMessages());
- assertEquals(4, notificationData.getSummaryOverflowMessagesCount());
- }
-
- private Account createFakeAccount() {
- Account account = mock(Account.class);
- when(account.getAccountNumber()).thenReturn(ACCOUNT_NUMBER);
- return account;
- }
-
- private MessageReference createMessageReference(String uid) {
- return new MessageReference(ACCOUNT_UUID, FOLDER_ID, uid, null);
- }
-
- private NotificationContent createNotificationContent(String uid) {
- MessageReference messageReference = createMessageReference(uid);
- return createNotificationContent(messageReference);
- }
-
- private NotificationContent createNotificationContent(MessageReference messageReference) {
- return new NotificationContent(messageReference, "", "", "", "", false);
- }
-
- private NotificationContent createNotificationContentForStarredMessage() {
- MessageReference messageReference = createMessageReference("42");
- return new NotificationContent(messageReference, "", "", "", "", true);
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/NotificationIdsTest.kt b/app/core/src/test/java/com/fsck/k9/notification/NotificationIdsTest.kt
index 4a39e06dc98720a25ca6a6c56914f87c2f50a013..9c1946b573f1f1e4d344f1f25b1f7a3ac40cdbdb 100644
--- a/app/core/src/test/java/com/fsck/k9/notification/NotificationIdsTest.kt
+++ b/app/core/src/test/java/com/fsck/k9/notification/NotificationIdsTest.kt
@@ -94,8 +94,8 @@ class NotificationIdsTest {
NotificationIds.getAuthenticationErrorNotificationId(account, false),
NotificationIds.getFetchingMailNotificationId(account),
NotificationIds.getNewMailSummaryNotificationId(account),
- ) + (0 until NotificationData.MAX_NUMBER_OF_STACKED_NOTIFICATIONS).map { index ->
- NotificationIds.getNewMailStackedNotificationId(account, index)
+ ) + (0 until MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS).map { index ->
+ NotificationIds.getSingleMessageNotificationId(account, index)
}
}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/RemoveNotificationResultTest.java b/app/core/src/test/java/com/fsck/k9/notification/RemoveNotificationResultTest.java
deleted file mode 100644
index 1a90b0594c3824e1f471afd2a7f4e648534f9f7a..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/RemoveNotificationResultTest.java
+++ /dev/null
@@ -1,107 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-
-public class RemoveNotificationResultTest {
- private static final int NOTIFICATION_ID = 23;
-
-
- private NotificationHolder notificationHolder;
-
-
- @Before
- public void setUp() throws Exception {
- notificationHolder = new NotificationHolder(NOTIFICATION_ID, null);
- }
-
- @Test
- public void createNotification_shouldCancelNotification_shouldReturnTrue() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.createNotification(notificationHolder);
-
- assertTrue(result.shouldCreateNotification());
- }
-
- @Test
- public void createNotification_getNotificationId_shouldReturnNotificationId() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.createNotification(notificationHolder);
-
- assertEquals(NOTIFICATION_ID, result.getNotificationId());
- }
-
- @Test
- public void createNotification_isUnknownNotification_shouldReturnFalse() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.createNotification(notificationHolder);
-
- assertFalse(result.isUnknownNotification());
- }
-
- @Test
- public void createNotification_getNotificationHolder_shouldReturnNotificationHolder() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.createNotification(notificationHolder);
-
- assertEquals(notificationHolder, result.getNotificationHolder());
- }
-
- @Test
- public void cancelNotification_shouldCancelNotification_shouldReturnFalse() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.cancelNotification(NOTIFICATION_ID);
-
- assertFalse(result.shouldCreateNotification());
- }
-
- @Test
- public void cancelNotification_getNotificationId_shouldReturnNotificationId() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.cancelNotification(NOTIFICATION_ID);
-
- assertEquals(NOTIFICATION_ID, result.getNotificationId());
- }
-
- @Test
- public void cancelNotification_isUnknownNotification_shouldReturnFalse() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.cancelNotification(NOTIFICATION_ID);
-
- assertFalse(result.isUnknownNotification());
- }
-
- @Test(expected = IllegalStateException.class)
- public void cancelNotification_getNotificationHolder_shouldThrowException() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.cancelNotification(NOTIFICATION_ID);
-
- result.getNotificationHolder();
- }
-
- @Test
- public void unknownNotification_shouldCancelNotification_shouldReturnFalse() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.unknownNotification();
-
- assertFalse(result.shouldCreateNotification());
- }
-
- @Test(expected = IllegalStateException.class)
- public void unknownNotification_getNotificationId_shouldThrowException() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.unknownNotification();
-
- result.getNotificationId();
- }
-
- @Test
- public void unknownNotification_isUnknownNotification_shouldReturnTrue() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.unknownNotification();
-
- assertTrue(result.isUnknownNotification());
- }
-
- @Test(expected = IllegalStateException.class)
- public void unknownNotification_getNotificationHolder_shouldThrowException() throws Exception {
- RemoveNotificationResult result = RemoveNotificationResult.unknownNotification();
-
- result.getNotificationHolder();
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..707670d110298796bccf52f0b78a224a1ebed891
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt
@@ -0,0 +1,93 @@
+package com.fsck.k9.notification
+
+import android.app.Notification
+import android.app.PendingIntent
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.core.app.ApplicationProvider
+import com.fsck.k9.Account
+import com.fsck.k9.RobolectricTest
+import com.fsck.k9.testing.MockHelper.mockBuilder
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+
+private const val ACCOUNT_NUMBER = 1
+private const val ACCOUNT_NAME = "TestAccount"
+
+class SendFailedNotificationControllerTest : RobolectricTest() {
+ private val resourceProvider: NotificationResourceProvider = TestNotificationResourceProvider()
+ private val notification = mock()
+ private val lockScreenNotification = mock()
+ private val notificationManager = mock()
+ private val builder = createFakeNotificationBuilder(notification)
+ private val lockScreenNotificationBuilder = createFakeNotificationBuilder(lockScreenNotification)
+ private val account = createFakeAccount()
+ private val contentIntent = mock()
+ private val notificationId = NotificationIds.getSendFailedNotificationId(account)
+ private val controller = SendFailedNotificationController(
+ notificationHelper = createFakeNotificationHelper(notificationManager, builder, lockScreenNotificationBuilder),
+ actionBuilder = createActionBuilder(contentIntent),
+ resourceProvider = resourceProvider
+ )
+
+ @Test
+ fun testShowSendFailedNotification() {
+ val exception = Exception()
+
+ controller.showSendFailedNotification(account, exception)
+
+ verify(notificationManager).notify(notificationId, notification)
+ verify(builder).setSmallIcon(resourceProvider.iconWarning)
+ verify(builder).setTicker("Failed to send some messages")
+ verify(builder).setContentTitle("Failed to send some messages")
+ verify(builder).setContentText("Exception")
+ verify(builder).setContentIntent(contentIntent)
+ verify(builder).setPublicVersion(lockScreenNotification)
+ verify(lockScreenNotificationBuilder).setContentTitle("Failed to send some messages")
+ verify(lockScreenNotificationBuilder, never()).setContentText(any())
+ verify(lockScreenNotificationBuilder, never()).setTicker(any())
+ }
+
+ @Test
+ fun testClearSendFailedNotification() {
+ controller.clearSendFailedNotification(account)
+
+ verify(notificationManager).cancel(notificationId)
+ }
+
+ private fun createFakeNotificationBuilder(notification: Notification): NotificationCompat.Builder {
+ return mockBuilder {
+ on { build() } doReturn notification
+ }
+ }
+
+ private fun createFakeNotificationHelper(
+ notificationManager: NotificationManagerCompat,
+ notificationBuilder: NotificationCompat.Builder,
+ lockScreenNotificationBuilder: NotificationCompat.Builder
+ ): NotificationHelper {
+ return mock {
+ on { getContext() } doReturn ApplicationProvider.getApplicationContext()
+ on { getNotificationManager() } doReturn notificationManager
+ on { createNotificationBuilder(any(), any()) }.doReturn(notificationBuilder, lockScreenNotificationBuilder)
+ }
+ }
+
+ private fun createFakeAccount(): Account {
+ return mock {
+ on { accountNumber } doReturn ACCOUNT_NUMBER
+ on { description } doReturn ACCOUNT_NAME
+ }
+ }
+
+ private fun createActionBuilder(contentIntent: PendingIntent): NotificationActionCreator {
+ return mock {
+ on { createViewFolderListPendingIntent(any(), anyInt()) } doReturn contentIntent
+ }
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationsTest.java b/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationsTest.java
deleted file mode 100644
index 5e6f617fc0ff09f50b4ca6897ea191d3a39168f1..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationsTest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.Builder;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.testing.MockHelper;
-import com.fsck.k9.RobolectricTest;
-import org.junit.Before;
-import org.junit.Test;
-import org.robolectric.RuntimeEnvironment;
-
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-
-public class SendFailedNotificationsTest extends RobolectricTest {
- private static final int ACCOUNT_NUMBER = 1;
- private static final String ACCOUNT_NAME = "TestAccount";
-
-
- private NotificationResourceProvider resourceProvider = new TestNotificationResourceProvider();
- private Notification notification;
- private NotificationManagerCompat notificationManager;
- private Builder builder;
- private Account account;
- private SendFailedNotifications sendFailedNotifications;
- private PendingIntent contentIntent;
- private int notificationId;
-
-
- @Before
- public void setUp() throws Exception {
- notification = createFakeNotification();
- notificationManager = createFakeNotificationManager();
- builder = createFakeNotificationBuilder(notification);
- NotificationHelper notificationHelper = createFakeNotificationHelper(notificationManager, builder);
- account = createFakeAccount();
- contentIntent = createFakeContentIntent();
- NotificationActionCreator actionBuilder = createActionBuilder(contentIntent);
- notificationId = NotificationIds.getSendFailedNotificationId(account);
-
- sendFailedNotifications = new SendFailedNotifications(notificationHelper, actionBuilder, resourceProvider);
- }
-
- @Test
- public void testShowSendFailedNotification() throws Exception {
- Exception exception = new Exception();
-
- sendFailedNotifications.showSendFailedNotification(account, exception);
-
- verify(notificationManager).notify(notificationId, notification);
- verify(builder).setSmallIcon(resourceProvider.getIconWarning());
- verify(builder).setTicker("Failed to send some messages");
- verify(builder).setContentTitle("Failed to send some messages");
- verify(builder).setContentText("Exception");
- verify(builder).setContentIntent(contentIntent);
- verify(builder).setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
- }
-
- @Test
- public void testClearSendFailedNotification() throws Exception {
- sendFailedNotifications.clearSendFailedNotification(account);
-
- verify(notificationManager).cancel(notificationId);
- }
-
- private Notification createFakeNotification() {
- return mock(Notification.class);
- }
-
- private NotificationManagerCompat createFakeNotificationManager() {
- return mock(NotificationManagerCompat.class);
- }
-
- private Builder createFakeNotificationBuilder(Notification notification) {
- Builder builder = MockHelper.mockBuilder(Builder.class);
- when(builder.build()).thenReturn(notification);
- return builder;
- }
-
- private NotificationHelper createFakeNotificationHelper(NotificationManagerCompat notificationManager,
- Builder builder) {
- NotificationHelper notificationHelper = mock(NotificationHelper.class);
- when(notificationHelper.getContext()).thenReturn(RuntimeEnvironment.application);
- when(notificationHelper.getNotificationManager()).thenReturn(notificationManager);
- when(notificationHelper.createNotificationBuilder(any(Account.class),
- any(NotificationChannelManager.ChannelType.class)))
- .thenReturn(builder);
-
- return notificationHelper;
- }
-
- private Account createFakeAccount() {
- Account account = mock(Account.class);
- when(account.getAccountNumber()).thenReturn(ACCOUNT_NUMBER);
- when(account.getDescription()).thenReturn(ACCOUNT_NAME);
-
- return account;
- }
-
- private PendingIntent createFakeContentIntent() {
- return mock(PendingIntent.class);
- }
-
- private NotificationActionCreator createActionBuilder(PendingIntent contentIntent) {
- NotificationActionCreator actionBuilder = mock(NotificationActionCreator.class);
- when(actionBuilder.createViewFolderListPendingIntent(any(Account.class), anyInt())).thenReturn(contentIntent);
- return actionBuilder;
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/SingleMessageNotificationDataCreatorTest.kt b/app/core/src/test/java/com/fsck/k9/notification/SingleMessageNotificationDataCreatorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2d9e92e95d3e7f087a82be440d8234873dbe9aaa
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/SingleMessageNotificationDataCreatorTest.kt
@@ -0,0 +1,282 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.K9
+import com.fsck.k9.K9.NotificationQuickDelete
+import com.fsck.k9.controller.MessageReference
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class SingleMessageNotificationDataCreatorTest {
+ private val account = createAccount()
+ private val notificationDataCreator = SingleMessageNotificationDataCreator()
+
+ @Test
+ fun `base properties`() {
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 23,
+ content = content,
+ timestamp = 9000,
+ addLockScreenNotification = true
+ )
+
+ assertThat(result.notificationId).isEqualTo(23)
+ assertThat(result.isSilent).isTrue()
+ assertThat(result.timestamp).isEqualTo(9000)
+ assertThat(result.content).isEqualTo(content)
+ assertThat(result.addLockScreenNotification).isTrue()
+ }
+
+ @Test
+ fun `summary notification base properties`() {
+ val content = createNotificationContent()
+ val notificationData = createNotificationData(content)
+
+ val result = notificationDataCreator.createSummarySingleNotificationData(
+ timestamp = 9000,
+ silent = false,
+ data = notificationData
+ )
+
+ assertThat(result.singleNotificationData.notificationId).isEqualTo(
+ NotificationIds.getNewMailSummaryNotificationId(account)
+ )
+ assertThat(result.singleNotificationData.isSilent).isFalse()
+ assertThat(result.singleNotificationData.timestamp).isEqualTo(9000)
+ assertThat(result.singleNotificationData.content).isEqualTo(content)
+ assertThat(result.singleNotificationData.addLockScreenNotification).isFalse()
+ }
+
+ @Test
+ fun `default actions`() {
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.actions).contains(NotificationAction.Reply)
+ assertThat(result.actions).contains(NotificationAction.MarkAsRead)
+ assertThat(result.wearActions).contains(WearNotificationAction.Reply)
+ assertThat(result.wearActions).contains(WearNotificationAction.MarkAsRead)
+ }
+
+ @Test
+ fun `always show delete action without confirmation`() {
+ setDeleteAction(NotificationQuickDelete.ALWAYS)
+ setConfirmDeleteFromNotification(false)
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.actions).contains(NotificationAction.Delete)
+ assertThat(result.wearActions).contains(WearNotificationAction.Delete)
+ }
+
+ @Test
+ fun `always show delete action with confirmation`() {
+ setDeleteAction(NotificationQuickDelete.ALWAYS)
+ setConfirmDeleteFromNotification(true)
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.actions).contains(NotificationAction.Delete)
+ assertThat(result.wearActions).doesNotContain(WearNotificationAction.Delete)
+ }
+
+ @Test
+ fun `show delete action for single notification without confirmation`() {
+ setDeleteAction(NotificationQuickDelete.FOR_SINGLE_MSG)
+ setConfirmDeleteFromNotification(false)
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.actions).contains(NotificationAction.Delete)
+ assertThat(result.wearActions).contains(WearNotificationAction.Delete)
+ }
+
+ @Test
+ fun `show delete action for single notification with confirmation`() {
+ setDeleteAction(NotificationQuickDelete.FOR_SINGLE_MSG)
+ setConfirmDeleteFromNotification(true)
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.actions).contains(NotificationAction.Delete)
+ assertThat(result.wearActions).doesNotContain(WearNotificationAction.Delete)
+ }
+
+ @Test
+ fun `never show delete action`() {
+ setDeleteAction(NotificationQuickDelete.NEVER)
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.actions).doesNotContain(NotificationAction.Delete)
+ assertThat(result.wearActions).doesNotContain(WearNotificationAction.Delete)
+ }
+
+ @Test
+ fun `archive action with archive folder`() {
+ account.archiveFolderId = 1
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.wearActions).contains(WearNotificationAction.Archive)
+ }
+
+ @Test
+ fun `archive action without archive folder`() {
+ account.archiveFolderId = null
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.wearActions).doesNotContain(WearNotificationAction.Archive)
+ }
+
+ @Test
+ fun `spam action with spam folder and without spam confirmation`() {
+ account.spamFolderId = 1
+ setConfirmSpam(false)
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.wearActions).contains(WearNotificationAction.Spam)
+ }
+
+ @Test
+ fun `spam action with spam folder and with spam confirmation`() {
+ account.spamFolderId = 1
+ setConfirmSpam(true)
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.wearActions).doesNotContain(WearNotificationAction.Spam)
+ }
+
+ @Test
+ fun `spam action without spam folder and without spam confirmation`() {
+ account.spamFolderId = null
+ setConfirmSpam(false)
+ val content = createNotificationContent()
+
+ val result = notificationDataCreator.createSingleNotificationData(
+ account = account,
+ notificationId = 0,
+ content = content,
+ timestamp = 0,
+ addLockScreenNotification = false
+ )
+
+ assertThat(result.wearActions).doesNotContain(WearNotificationAction.Spam)
+ }
+
+ private fun setDeleteAction(mode: NotificationQuickDelete) {
+ K9.notificationQuickDeleteBehaviour = mode
+ }
+
+ private fun setConfirmDeleteFromNotification(confirm: Boolean) {
+ K9.isConfirmDeleteFromNotification = confirm
+ }
+
+ private fun setConfirmSpam(confirm: Boolean) {
+ K9.isConfirmSpam = confirm
+ }
+
+ private fun createAccount(): Account {
+ return Account("00000000-0000-0000-0000-000000000000").apply {
+ accountNumber = 42
+ }
+ }
+
+ private fun createNotificationContent() = NotificationContent(
+ messageReference = MessageReference("irrelevant", 1, "irrelevant"),
+ sender = "irrelevant",
+ subject = "irrelevant",
+ preview = "irrelevant",
+ summary = "irrelevant"
+ )
+
+ private fun createNotificationData(content: NotificationContent): NotificationData {
+ return NotificationData(
+ account,
+ activeNotifications = listOf(
+ NotificationHolder(
+ notificationId = 1,
+ timestamp = 0,
+ content = content
+ )
+ ),
+ inactiveNotifications = emptyList()
+ )
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..216d20c1ae959926152dbfe2ea6482fef51f8ae5
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/SummaryNotificationDataCreatorTest.kt
@@ -0,0 +1,278 @@
+package com.fsck.k9.notification
+
+import com.fsck.k9.Account
+import com.fsck.k9.Clock
+import com.fsck.k9.K9
+import com.fsck.k9.TestClock
+import com.fsck.k9.controller.MessageReference
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.dsl.module
+
+private val TIMESTAMP = 0L
+
+class SummaryNotificationDataCreatorTest {
+ private val account = createAccount()
+ private val notificationDataCreator = SummaryNotificationDataCreator(SingleMessageNotificationDataCreator())
+
+ @Before
+ fun setUp() {
+ startKoin {
+ modules(
+ module {
+ single { TestClock() }
+ }
+ )
+ }
+ }
+
+ @After
+ fun tearDown() {
+ stopKoin()
+ setQuietTime(false)
+ }
+
+ @Test
+ fun `single new message`() {
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = false
+ )
+
+ assertThat(result).isInstanceOf(SummarySingleNotificationData::class.java)
+ }
+
+ @Test
+ fun `single notification during quiet time`() {
+ setQuietTime(true)
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = false
+ )
+
+ val summaryNotificationData = result as SummarySingleNotificationData
+ assertThat(summaryNotificationData.singleNotificationData.isSilent).isTrue()
+ }
+
+ @Test
+ fun `single notification with quiet time disabled`() {
+ setQuietTime(false)
+ val notificationData = createNotificationData()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = false
+ )
+
+ val summaryNotificationData = result as SummarySingleNotificationData
+ assertThat(summaryNotificationData.singleNotificationData.isSilent).isFalse()
+ }
+
+ @Test
+ fun `inbox-style notification during quiet time`() {
+ setQuietTime(true)
+ val notificationData = createNotificationDataWithMultipleMessages()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = false
+ )
+
+ val summaryNotificationData = result as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.isSilent).isTrue()
+ }
+
+ @Test
+ fun `inbox-style notification with quiet time disabled`() {
+ setQuietTime(false)
+ val notificationData = createNotificationDataWithMultipleMessages()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = false
+ )
+
+ val summaryNotificationData = result as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.isSilent).isFalse()
+ }
+
+ @Test
+ fun `inbox-style base properties`() {
+ val notificationData = createNotificationDataWithMultipleMessages()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = true
+ )
+
+ val summaryNotificationData = result as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.notificationId).isEqualTo(
+ NotificationIds.getNewMailSummaryNotificationId(account)
+ )
+ assertThat(summaryNotificationData.isSilent).isTrue()
+ assertThat(summaryNotificationData.timestamp).isEqualTo(TIMESTAMP)
+ }
+
+ @Test
+ fun `default actions`() {
+ val notificationData = createNotificationDataWithMultipleMessages()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = true
+ )
+
+ val summaryNotificationData = result as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.actions).contains(SummaryNotificationAction.MarkAsRead)
+ assertThat(summaryNotificationData.wearActions).contains(SummaryWearNotificationAction.MarkAsRead)
+ }
+
+ @Test
+ fun `always show delete action without confirmation`() {
+ setDeleteAction(K9.NotificationQuickDelete.ALWAYS)
+ setConfirmDeleteFromNotification(false)
+ val notificationData = createNotificationDataWithMultipleMessages()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = true
+ )
+
+ val summaryNotificationData = result as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.actions).contains(SummaryNotificationAction.Delete)
+ assertThat(summaryNotificationData.wearActions).contains(SummaryWearNotificationAction.Delete)
+ }
+
+ @Test
+ fun `always show delete action with confirmation`() {
+ setDeleteAction(K9.NotificationQuickDelete.ALWAYS)
+ setConfirmDeleteFromNotification(true)
+ val notificationData = createNotificationDataWithMultipleMessages()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = true
+ )
+
+ val summaryNotificationData = result as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.actions).contains(SummaryNotificationAction.Delete)
+ assertThat(summaryNotificationData.wearActions).doesNotContain(SummaryWearNotificationAction.Delete)
+ }
+
+ @Test
+ fun `show delete action for single notification without confirmation`() {
+ setDeleteAction(K9.NotificationQuickDelete.FOR_SINGLE_MSG)
+ setConfirmDeleteFromNotification(false)
+ val notificationData = createNotificationDataWithMultipleMessages()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = true
+ )
+
+ val summaryNotificationData = result as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.actions).doesNotContain(SummaryNotificationAction.Delete)
+ assertThat(summaryNotificationData.wearActions).doesNotContain(SummaryWearNotificationAction.Delete)
+ }
+
+ @Test
+ fun `never show delete action`() {
+ setDeleteAction(K9.NotificationQuickDelete.NEVER)
+ val notificationData = createNotificationDataWithMultipleMessages()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = true
+ )
+
+ val summaryNotificationData = result as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.actions).doesNotContain(SummaryNotificationAction.Delete)
+ assertThat(summaryNotificationData.wearActions).doesNotContain(SummaryWearNotificationAction.Delete)
+ }
+
+ @Test
+ fun `archive action with archive folder`() {
+ account.archiveFolderId = 1
+ val notificationData = createNotificationDataWithMultipleMessages()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = true
+ )
+
+ val summaryNotificationData = result as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.wearActions).contains(SummaryWearNotificationAction.Archive)
+ }
+
+ @Test
+ fun `archive action without archive folder`() {
+ account.archiveFolderId = null
+ val notificationData = createNotificationDataWithMultipleMessages()
+
+ val result = notificationDataCreator.createSummaryNotificationData(
+ notificationData,
+ silent = true
+ )
+
+ val summaryNotificationData = result as SummaryInboxNotificationData
+ assertThat(summaryNotificationData.wearActions).doesNotContain(SummaryWearNotificationAction.Archive)
+ }
+
+ private fun setQuietTime(quietTime: Boolean) {
+ K9.isQuietTimeEnabled = quietTime
+ if (quietTime) {
+ K9.quietTimeStarts = "0:00"
+ K9.quietTimeEnds = "23:59"
+ }
+ }
+
+ private fun setDeleteAction(mode: K9.NotificationQuickDelete) {
+ K9.notificationQuickDeleteBehaviour = mode
+ }
+
+ private fun setConfirmDeleteFromNotification(confirm: Boolean) {
+ K9.isConfirmDeleteFromNotification = confirm
+ }
+
+ private fun createAccount(): Account {
+ return Account("00000000-0000-0000-0000-000000000000").apply {
+ accountNumber = 42
+ }
+ }
+
+ private fun createNotificationContent() = NotificationContent(
+ messageReference = MessageReference("irrelevant", 1, "irrelevant"),
+ sender = "irrelevant",
+ subject = "irrelevant",
+ preview = "irrelevant",
+ summary = "irrelevant"
+ )
+
+ private fun createNotificationData(
+ contentList: List = listOf(createNotificationContent())
+ ): NotificationData {
+ val activeNotifications = contentList.mapIndexed { index, content ->
+ NotificationHolder(notificationId = index, TIMESTAMP, content)
+ }
+
+ return NotificationData(account, activeNotifications, inactiveNotifications = emptyList())
+ }
+
+ @OptIn(ExperimentalStdlibApi::class)
+ private fun createNotificationDataWithMultipleMessages(times: Int = 2): NotificationData {
+ val contentList = buildList {
+ repeat(times) {
+ add(createNotificationContent())
+ }
+ }
+ return createNotificationData(contentList)
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..417643a1f05a2a094ce376ea0a3de166e456b60b
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt
@@ -0,0 +1,153 @@
+package com.fsck.k9.notification
+
+import android.app.Notification
+import android.app.PendingIntent
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.core.app.ApplicationProvider
+import com.fsck.k9.Account
+import com.fsck.k9.RobolectricTest
+import com.fsck.k9.mailstore.LocalFolder
+import com.fsck.k9.notification.NotificationIds.getFetchingMailNotificationId
+import com.fsck.k9.testing.MockHelper.mockBuilder
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+
+private const val ACCOUNT_NUMBER = 1
+private const val ACCOUNT_NAME = "TestAccount"
+private const val FOLDER_SERVER_ID = "INBOX"
+private const val FOLDER_NAME = "Inbox"
+
+class SyncNotificationControllerTest : RobolectricTest() {
+ private val resourceProvider: NotificationResourceProvider = TestNotificationResourceProvider()
+ private val notification = mock()
+ private val lockScreenNotification = mock()
+ private val notificationManager = mock()
+ private val builder = createFakeNotificationBuilder(notification)
+ private val lockScreenNotificationBuilder = createFakeNotificationBuilder(lockScreenNotification)
+ private val account = createFakeAccount()
+ private val contentIntent = mock()
+ private val controller = SyncNotificationController(
+ notificationHelper = createFakeNotificationHelper(notificationManager, builder, lockScreenNotificationBuilder),
+ actionBuilder = createActionBuilder(contentIntent),
+ resourceProvider = resourceProvider
+ )
+
+ @Test
+ fun testShowSendingNotification() {
+ val notificationId = getFetchingMailNotificationId(account)
+
+ controller.showSendingNotification(account)
+
+ verify(notificationManager).notify(notificationId, notification)
+ verify(builder).setSmallIcon(resourceProvider.iconSendingMail)
+ verify(builder).setTicker("Sending mail: $ACCOUNT_NAME")
+ verify(builder).setContentTitle("Sending mail")
+ verify(builder).setContentText(ACCOUNT_NAME)
+ verify(builder).setContentIntent(contentIntent)
+ verify(builder).setPublicVersion(lockScreenNotification)
+ verify(lockScreenNotificationBuilder).setContentTitle("Sending mail")
+ verify(lockScreenNotificationBuilder, never()).setContentText(any())
+ verify(lockScreenNotificationBuilder, never()).setTicker(any())
+ }
+
+ @Test
+ fun testClearSendingNotification() {
+ val notificationId = getFetchingMailNotificationId(account)
+
+ controller.clearSendingNotification(account)
+
+ verify(notificationManager).cancel(notificationId)
+ }
+
+ @Test
+ fun testGetFetchingMailNotificationId() {
+ val localFolder = createFakeLocalFolder()
+ val notificationId = getFetchingMailNotificationId(account)
+
+ controller.showFetchingMailNotification(account, localFolder)
+
+ verify(notificationManager).notify(notificationId, notification)
+ verify(builder).setSmallIcon(resourceProvider.iconCheckingMail)
+ verify(builder).setTicker("Checking mail: $ACCOUNT_NAME:$FOLDER_NAME")
+ verify(builder).setContentTitle("Checking mail")
+ verify(builder).setContentText("$ACCOUNT_NAME:$FOLDER_NAME")
+ verify(builder).setContentIntent(contentIntent)
+ verify(builder).setPublicVersion(lockScreenNotification)
+ verify(lockScreenNotificationBuilder).setContentTitle("Checking mail")
+ verify(lockScreenNotificationBuilder, never()).setContentText(any())
+ verify(lockScreenNotificationBuilder, never()).setTicker(any())
+ }
+
+ @Test
+ fun testShowEmptyFetchingMailNotification() {
+ val notificationId = getFetchingMailNotificationId(account)
+
+ controller.showEmptyFetchingMailNotification(account)
+
+ verify(notificationManager).notify(notificationId, notification)
+ verify(builder).setSmallIcon(resourceProvider.iconCheckingMail)
+ verify(builder).setContentTitle("Checking mail")
+ verify(builder).setContentText(ACCOUNT_NAME)
+ verify(builder).setPublicVersion(lockScreenNotification)
+ verify(lockScreenNotificationBuilder).setContentTitle("Checking mail")
+ verify(lockScreenNotificationBuilder, never()).setContentText(any())
+ verify(lockScreenNotificationBuilder, never()).setTicker(any())
+ }
+
+ @Test
+ fun testClearSendFailedNotification() {
+ val notificationId = getFetchingMailNotificationId(account)
+
+ controller.clearFetchingMailNotification(account)
+
+ verify(notificationManager).cancel(notificationId)
+ }
+
+ private fun createFakeNotificationBuilder(notification: Notification): NotificationCompat.Builder {
+ return mockBuilder {
+ on { build() } doReturn notification
+ }
+ }
+
+ private fun createFakeNotificationHelper(
+ notificationManager: NotificationManagerCompat,
+ notificationBuilder: NotificationCompat.Builder,
+ lockScreenNotificationBuilder: NotificationCompat.Builder
+ ): NotificationHelper {
+ return mock {
+ on { getContext() } doReturn ApplicationProvider.getApplicationContext()
+ on { getNotificationManager() } doReturn notificationManager
+ on { createNotificationBuilder(any(), any()) }.doReturn(notificationBuilder, lockScreenNotificationBuilder)
+ on { getAccountName(any()) } doReturn ACCOUNT_NAME
+ }
+ }
+
+ private fun createFakeAccount(): Account {
+ return mock {
+ on { accountNumber } doReturn ACCOUNT_NUMBER
+ on { description } doReturn ACCOUNT_NAME
+ on { outboxFolderId } doReturn 33L
+ }
+ }
+
+ private fun createActionBuilder(contentIntent: PendingIntent): NotificationActionCreator {
+ return mock {
+ on { createViewFolderPendingIntent(eq(account), anyLong(), anyInt()) } doReturn contentIntent
+ }
+ }
+
+ private fun createFakeLocalFolder(): LocalFolder {
+ return mock {
+ on { serverId } doReturn FOLDER_SERVER_ID
+ on { name } doReturn FOLDER_NAME
+ }
+ }
+}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationsTest.java b/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationsTest.java
deleted file mode 100644
index 4241d17ec1f4117b4b1df63b7dd11eddcada5c5c..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationsTest.java
+++ /dev/null
@@ -1,160 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.Builder;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.mailstore.LocalFolder;
-import com.fsck.k9.testing.MockHelper;
-import com.fsck.k9.RobolectricTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.robolectric.RuntimeEnvironment;
-
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-
-public class SyncNotificationsTest extends RobolectricTest {
- private static final int ACCOUNT_NUMBER = 1;
- private static final String ACCOUNT_NAME = "TestAccount";
- private static final String FOLDER_SERVER_ID = "INBOX";
- private static final String FOLDER_NAME = "Inbox";
-
-
- private NotificationResourceProvider resourceProvider = new TestNotificationResourceProvider();
- private Notification notification;
- private NotificationManagerCompat notificationManager;
- private Builder builder;
- private Account account;
- private SyncNotifications syncNotifications;
- private PendingIntent contentIntent;
-
-
- @Before
- public void setUp() throws Exception {
- notification = createFakeNotification();
- notificationManager = createFakeNotificationManager();
- builder = createFakeNotificationBuilder(notification);
- NotificationHelper notificationHelper = createFakeNotificationHelper(notificationManager, builder);
- account = createFakeAccount();
- contentIntent = createFakeContentIntent();
- NotificationActionCreator actionBuilder = createActionBuilder(contentIntent);
-
- syncNotifications = new SyncNotifications(notificationHelper, actionBuilder, resourceProvider);
- }
-
- @Test
- public void testShowSendingNotification() throws Exception {
- int notificationId = NotificationIds.getFetchingMailNotificationId(account);
-
- syncNotifications.showSendingNotification(account);
-
- verify(notificationManager).notify(notificationId, notification);
- verify(builder).setSmallIcon(resourceProvider.getIconSendingMail());
- verify(builder).setTicker("Sending mail: " + ACCOUNT_NAME);
- verify(builder).setContentTitle("Sending mail");
- verify(builder).setContentText(ACCOUNT_NAME);
- verify(builder).setContentIntent(contentIntent);
- verify(builder).setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
- }
-
- @Test
- public void testClearSendingNotification() throws Exception {
- int notificationId = NotificationIds.getFetchingMailNotificationId(account);
-
- syncNotifications.clearSendingNotification(account);
-
- verify(notificationManager).cancel(notificationId);
- }
-
- @Test
- public void testGetFetchingMailNotificationId() throws Exception {
- LocalFolder localFolder = createFakeLocalFolder();
- int notificationId = NotificationIds.getFetchingMailNotificationId(account);
-
- syncNotifications.showFetchingMailNotification(account, localFolder);
-
- verify(notificationManager).notify(notificationId, notification);
- verify(builder).setSmallIcon(resourceProvider.getIconCheckingMail());
- verify(builder).setTicker("Checking mail: " + ACCOUNT_NAME + ":" + FOLDER_NAME);
- verify(builder).setContentTitle("Checking mail");
- verify(builder).setContentText(ACCOUNT_NAME + ":" + FOLDER_NAME);
- verify(builder).setContentIntent(contentIntent);
- verify(builder).setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
- }
-
- @Test
- public void testClearSendFailedNotification() throws Exception {
- int notificationId = NotificationIds.getFetchingMailNotificationId(account);
-
- syncNotifications.clearFetchingMailNotification(account);
-
- verify(notificationManager).cancel(notificationId);
- }
-
-
- private Notification createFakeNotification() {
- return mock(Notification.class);
- }
-
- private NotificationManagerCompat createFakeNotificationManager() {
- return mock(NotificationManagerCompat.class);
- }
-
- private Builder createFakeNotificationBuilder(Notification notification) {
- Builder builder = MockHelper.mockBuilder(Builder.class);
- when(builder.build()).thenReturn(notification);
- return builder;
- }
-
- private NotificationHelper createFakeNotificationHelper(
- NotificationManagerCompat notificationManager, Builder builder) {
- NotificationHelper notificationHelper = mock(NotificationHelper.class);
- when(notificationHelper.getContext()).thenReturn(RuntimeEnvironment.application);
- when(notificationHelper.getNotificationManager()).thenReturn(notificationManager);
- when(notificationHelper.createNotificationBuilder(any(Account.class),
- any(NotificationChannelManager.ChannelType.class)))
- .thenReturn(builder);
- when(notificationHelper.getAccountName(any(Account.class))).thenReturn(ACCOUNT_NAME);
-
- return notificationHelper;
- }
-
- private Account createFakeAccount() {
- Account account = mock(Account.class);
- when(account.getAccountNumber()).thenReturn(ACCOUNT_NUMBER);
- when(account.getDescription()).thenReturn(ACCOUNT_NAME);
- when(account.getOutboxFolderId()).thenReturn(33L);
-
- return account;
- }
-
- private PendingIntent createFakeContentIntent() {
- return mock(PendingIntent.class);
- }
-
- private NotificationActionCreator createActionBuilder(PendingIntent contentIntent) {
- NotificationActionCreator actionBuilder = mock(NotificationActionCreator.class);
- when(actionBuilder.createViewFolderPendingIntent(eq(account), anyLong(), anyInt()))
- .thenReturn(contentIntent);
- return actionBuilder;
- }
-
- private LocalFolder createFakeLocalFolder() {
- LocalFolder folder = mock(LocalFolder.class);
- when(folder.getServerId()).thenReturn(FOLDER_SERVER_ID);
- when(folder.getName()).thenReturn(FOLDER_NAME);
- return folder;
- }
-}
diff --git a/app/core/src/test/java/com/fsck/k9/notification/TestNotificationResourceProvider.kt b/app/core/src/test/java/com/fsck/k9/notification/TestNotificationResourceProvider.kt
index 467d7387dc80ac2557ccdb4749519d52bdcb4457..ab235bbac5e74a93ce10a8ad5045b6df57575a99 100644
--- a/app/core/src/test/java/com/fsck/k9/notification/TestNotificationResourceProvider.kt
+++ b/app/core/src/test/java/com/fsck/k9/notification/TestNotificationResourceProvider.kt
@@ -26,6 +26,8 @@ class TestNotificationResourceProvider : NotificationResourceProvider {
override fun authenticationErrorBody(accountName: String): String =
"Authentication failed for $accountName. Update your server settings."
+ override fun certificateErrorTitle(): String = "Certificate error"
+
override fun certificateErrorTitle(accountName: String): String = "Certificate error for $accountName"
override fun certificateErrorBody(): String = "Check your server settings"
diff --git a/app/core/src/test/java/com/fsck/k9/notification/WearNotificationsTest.java b/app/core/src/test/java/com/fsck/k9/notification/WearNotificationsTest.java
deleted file mode 100644
index ea830f9b3eb0575a2780745ea4bdc8c7b8f9ba3d..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/notification/WearNotificationsTest.java
+++ /dev/null
@@ -1,362 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.Context;
-import androidx.core.app.NotificationCompat.Action;
-import androidx.core.app.NotificationCompat.Builder;
-import androidx.core.app.NotificationCompat.Extender;
-import androidx.core.app.NotificationCompat.WearableExtender;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.K9;
-import com.fsck.k9.K9.NotificationQuickDelete;
-import com.fsck.k9.testing.MockHelper;
-import com.fsck.k9.RobolectricTest;
-import com.fsck.k9.controller.MessageReference;
-import com.fsck.k9.controller.MessagingController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentMatcher;
-import org.robolectric.RuntimeEnvironment;
-
-import java.util.ArrayList;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.argThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-
-public class WearNotificationsTest extends RobolectricTest {
- private static final int ACCOUNT_NUMBER = 42;
- private static final String ACCOUNT_NAME = "accountName";
-
- private NotificationResourceProvider resourceProvider = new TestNotificationResourceProvider();
- private Account account;
- private Builder builder;
- private NotificationActionCreator actionCreator;
- private TestWearNotifications wearNotifications;
- private Notification notification;
-
- @Before
- public void setUp() throws Exception {
- account = createAccount();
- notification = createNotification();
- builder = createNotificationBuilder(notification);
- actionCreator = createNotificationActionCreator();
- NotificationHelper notificationHelper = createNotificationHelper(RuntimeEnvironment.application, builder);
- MessagingController messagingController = createMessagingController();
-
- wearNotifications = new TestWearNotifications(notificationHelper, actionCreator, messagingController,
- resourceProvider);
- }
-
- @Test
- public void testBuildStackedNotification() throws Exception {
- disableOptionalActions();
- int notificationIndex = 0;
- int notificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- MessageReference messageReference = createMessageReference(1);
- NotificationContent content = createNotificationContent(messageReference);
- NotificationHolder holder = createNotificationHolder(notificationId, content);
- PendingIntent replyPendingIntent = createFakePendingIntent(1);
- when(actionCreator.createReplyPendingIntent(messageReference, notificationId)).thenReturn(replyPendingIntent);
- PendingIntent markAsReadPendingIntent = createFakePendingIntent(2);
- when(actionCreator.createMarkMessageAsReadPendingIntent(messageReference, notificationId))
- .thenReturn(markAsReadPendingIntent);
-
- Notification result = wearNotifications.buildStackedNotification(account, holder);
-
- assertEquals(notification, result);
- verifyExtendWasOnlyCalledOnce();
- verifyAddAction(resourceProvider.getWearIconReplyAll(), "Reply", replyPendingIntent);
- verifyAddAction(resourceProvider.getWearIconMarkAsRead(), "Mark Read", markAsReadPendingIntent);
- verifyNumberOfActions(2);
- }
-
- @Test
- public void testBuildStackedNotificationWithDeleteActionEnabled() throws Exception {
- enableDeleteAction();
- int notificationIndex = 0;
- int notificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- MessageReference messageReference = createMessageReference(1);
- NotificationContent content = createNotificationContent(messageReference);
- NotificationHolder holder = createNotificationHolder(notificationId, content);
- PendingIntent deletePendingIntent = createFakePendingIntent(1);
- when(actionCreator.createDeleteMessagePendingIntent(messageReference, notificationId))
- .thenReturn(deletePendingIntent);
-
- Notification result = wearNotifications.buildStackedNotification(account, holder);
-
- assertEquals(notification, result);
- verifyExtendWasOnlyCalledOnce();
- verifyAddAction(resourceProvider.getWearIconDelete(), "Delete", deletePendingIntent);
- }
-
- @Test
- public void testBuildStackedNotificationWithArchiveActionEnabled() throws Exception {
- enableArchiveAction();
- int notificationIndex = 0;
- int notificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- MessageReference messageReference = createMessageReference(1);
- NotificationContent content = createNotificationContent(messageReference);
- NotificationHolder holder = createNotificationHolder(notificationId, content);
- PendingIntent archivePendingIntent = createFakePendingIntent(1);
- when(actionCreator.createArchiveMessagePendingIntent(messageReference, notificationId))
- .thenReturn(archivePendingIntent);
-
- Notification result = wearNotifications.buildStackedNotification(account, holder);
-
- assertEquals(notification, result);
- verifyExtendWasOnlyCalledOnce();
- verifyAddAction(resourceProvider.getWearIconArchive(), "Archive", archivePendingIntent);
- }
-
- @Test
- public void testBuildStackedNotificationWithMarkAsSpamActionEnabled() throws Exception {
- enableSpamAction();
- int notificationIndex = 0;
- int notificationId = NotificationIds.getNewMailStackedNotificationId(account, notificationIndex);
- MessageReference messageReference = createMessageReference(1);
- NotificationContent content = createNotificationContent(messageReference);
- NotificationHolder holder = createNotificationHolder(notificationId, content);
- PendingIntent markAsSpamPendingIntent = createFakePendingIntent(1);
- when(actionCreator.createMarkMessageAsSpamPendingIntent(messageReference, notificationId))
- .thenReturn(markAsSpamPendingIntent);
-
- Notification result = wearNotifications.buildStackedNotification(account, holder);
-
- assertEquals(notification, result);
- verifyExtendWasOnlyCalledOnce();
- verifyAddAction(resourceProvider.getWearIconMarkAsSpam(), "Spam", markAsSpamPendingIntent);
- }
-
- @Test
- public void testAddSummaryActions() throws Exception {
- disableOptionalSummaryActions();
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- ArrayList messageReferences = createMessageReferenceList();
- NotificationData notificationData = createNotificationData(messageReferences);
- PendingIntent markAllAsReadPendingIntent = createFakePendingIntent(1);
- when(actionCreator.createMarkAllAsReadPendingIntent(account, messageReferences, notificationId))
- .thenReturn(markAllAsReadPendingIntent);
-
- wearNotifications.addSummaryActions(builder, notificationData);
-
- verifyExtendWasOnlyCalledOnce();
- verifyAddAction(resourceProvider.getWearIconMarkAsRead(), "Mark All Read", markAllAsReadPendingIntent);
- verifyNumberOfActions(1);
- }
-
- @Test
- public void testAddSummaryActionsWithDeleteAllActionEnabled() throws Exception {
- enableDeleteAction();
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- ArrayList messageReferences = createMessageReferenceList();
- NotificationData notificationData = createNotificationData(messageReferences);
- PendingIntent deletePendingIntent = createFakePendingIntent(1);
- when(actionCreator.createDeleteAllPendingIntent(account, messageReferences, notificationId))
- .thenReturn(deletePendingIntent);
-
- wearNotifications.addSummaryActions(builder, notificationData);
-
- verifyExtendWasOnlyCalledOnce();
- verifyAddAction(resourceProvider.getWearIconDelete(), "Delete All", deletePendingIntent);
- }
-
- @Test
- public void testAddSummaryActionsWithArchiveAllActionEnabled() throws Exception {
- enableArchiveAction();
- int notificationId = NotificationIds.getNewMailSummaryNotificationId(account);
- ArrayList messageReferences = createMessageReferenceList();
- NotificationData notificationData = createNotificationData(messageReferences);
- PendingIntent archivePendingIntent = createFakePendingIntent(1);
- when(actionCreator.createArchiveAllPendingIntent(account, messageReferences, notificationId))
- .thenReturn(archivePendingIntent);
-
- wearNotifications.addSummaryActions(builder, notificationData);
-
- verifyExtendWasOnlyCalledOnce();
- verifyAddAction(resourceProvider.getWearIconArchive(), "Archive All", archivePendingIntent);
- }
-
- private void disableOptionalActions() {
- disableDeleteAction();
- disableArchiveAction();
- disableSpamAction();
- }
-
- private void disableDeleteAction() {
- K9.setNotificationQuickDeleteBehaviour(NotificationQuickDelete.NEVER);
- }
-
- private void disableArchiveAction() {
- when(account.getArchiveFolderId()).thenReturn(null);
- }
-
- private void disableSpamAction() {
- when(account.getSpamFolderId()).thenReturn(null);
- }
-
- private void enableDeleteAction() {
- K9.setNotificationQuickDeleteBehaviour(NotificationQuickDelete.ALWAYS);
- K9.setConfirmDeleteFromNotification(false);
- }
-
- private void enableArchiveAction() {
- when(account.getArchiveFolderId()).thenReturn(22L);
- }
-
- private void enableSpamAction() {
- when(account.getSpamFolderId()).thenReturn(11L);
- }
-
- private void disableOptionalSummaryActions() {
- disableDeleteAction();
- disableArchiveAction();
- }
-
- private Builder createNotificationBuilder(Notification notification) {
- Builder builder = MockHelper.mockBuilder(Builder.class);
- when(builder.build()).thenReturn(notification);
- return builder;
- }
-
- private NotificationHelper createNotificationHelper(Context context, Builder builder) {
- NotificationHelper notificationHelper = mock(NotificationHelper.class);
- when(notificationHelper.createNotificationBuilder(any(Account.class), any(NotificationChannelManager
- .ChannelType.class))).thenReturn(builder);
- when(notificationHelper.getAccountName(account)).thenReturn(ACCOUNT_NAME);
- when(notificationHelper.getContext()).thenReturn(context);
- return notificationHelper;
- }
-
- private NotificationActionCreator createNotificationActionCreator() {
- return mock(NotificationActionCreator.class);
- }
-
- private Account createAccount() {
- Account account = mock(Account.class);
- when(account.getAccountNumber()).thenReturn(ACCOUNT_NUMBER);
- return account;
- }
-
- private MessagingController createMessagingController() {
- MessagingController messagingController = mock(MessagingController.class);
- when(messagingController.isMoveCapable(account)).thenReturn(true);
- return messagingController;
- }
-
- private NotificationContent createNotificationContent(MessageReference messageReference) {
- return new NotificationContent(messageReference, null, null, null, null, false);
- }
-
- private NotificationHolder createNotificationHolder(int notificationId, NotificationContent content) {
- return new NotificationHolder(notificationId, content);
- }
-
- private Notification createNotification() {
- return mock(Notification.class);
- }
-
- private MessageReference createMessageReference(int number) {
- return new MessageReference("account", 1, String.valueOf(number), null);
- }
-
- private PendingIntent createFakePendingIntent(int requestCode) {
- return PendingIntent.getActivity(RuntimeEnvironment.application, requestCode, null, 0);
- }
-
- private ArrayList createMessageReferenceList() {
- ArrayList messageReferences = new ArrayList<>();
- messageReferences.add(createMessageReference(1));
- messageReferences.add(createMessageReference(2));
-
- return messageReferences;
- }
-
- private NotificationData createNotificationData(ArrayList messageReferences) {
- NotificationData notificationData = mock(NotificationData.class);
- when(notificationData.getAccount()).thenReturn(account);
- when(notificationData.getAllMessageReferences()).thenReturn(messageReferences);
- return notificationData;
- }
-
- private Builder verifyExtendWasOnlyCalledOnce() {
- return verify(builder, times(1)).extend(any(Extender.class));
- }
-
- private void verifyAddAction(int icon, String title, PendingIntent pendingIntent) {
- verify(builder).extend(action(icon, title, pendingIntent));
- }
-
- private Builder verifyNumberOfActions(int expectedNumberOfActions) {
- return verify(builder).extend(numberOfActions(expectedNumberOfActions));
- }
-
- private WearableExtender action(int icon, String title, PendingIntent pendingIntent) {
- return argThat(new ActionMatcher(icon, title, pendingIntent));
- }
-
- private WearableExtender numberOfActions(int expectedNumberOfActions) {
- return argThat(new NumberOfActionsMatcher(expectedNumberOfActions));
- }
-
-
- static class ActionMatcher implements ArgumentMatcher {
- private int icon;
- private String title;
- private PendingIntent pendingIntent;
-
- public ActionMatcher(int icon, String title, PendingIntent pendingIntent) {
- this.icon = icon;
- this.title = title;
- this.pendingIntent = pendingIntent;
- }
-
- @Override
- public boolean matches(WearableExtender argument) {
- for (Action action : argument.getActions()) {
- if (action.icon == icon && action.title.equals(title) && action.actionIntent == pendingIntent) {
- return true;
- }
- }
-
- return false;
- }
- }
-
- static class NumberOfActionsMatcher implements ArgumentMatcher {
- private final int expectedNumberOfActions;
-
- public NumberOfActionsMatcher(int expectedNumberOfActions) {
- this.expectedNumberOfActions = expectedNumberOfActions;
- }
-
- @Override
- public boolean matches(WearableExtender argument) {
- return argument.getActions().size() == expectedNumberOfActions;
- }
- }
-
- static class TestWearNotifications extends WearNotifications {
- private final MessagingController messagingController;
-
- public TestWearNotifications(NotificationHelper notificationHelper, NotificationActionCreator actionCreator,
- MessagingController messagingController, NotificationResourceProvider resourceProvider) {
- super(notificationHelper, actionCreator, resourceProvider);
- this.messagingController = messagingController;
- }
-
- @Override
- MessagingController createMessagingController() {
- return messagingController;
- }
- }
-}
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 d03f923d247b83459f58d77448aab690dc02b2ff..b92e017ba347e0ca4f0e637474d8893e2b3d7fe2 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
@@ -2,7 +2,7 @@ package com.fsck.k9.preferences
import com.fsck.k9.K9RobolectricTest
import com.fsck.k9.Preferences
-import com.fsck.k9.mailstore.FolderRepositoryManager
+import com.fsck.k9.mailstore.FolderRepository
import java.io.ByteArrayOutputStream
import org.jdom2.Document
import org.jdom2.input.SAXBuilder
@@ -17,12 +17,12 @@ class SettingsExporterTest : K9RobolectricTest() {
private val contentResolver = RuntimeEnvironment.application.contentResolver
private val preferences: Preferences by inject()
private val folderSettingsProvider: FolderSettingsProvider by inject()
- private val folderRepositoryManager: FolderRepositoryManager by inject()
+ private val folderRepository: FolderRepository by inject()
private val settingsExporter = SettingsExporter(
contentResolver,
preferences,
folderSettingsProvider,
- folderRepositoryManager
+ folderRepository
)
@Test
diff --git a/app/k9mail-jmap/build.gradle b/app/k9mail-jmap/build.gradle
index b6a3cf29f34f3bf367f6eeb4ec5dcc53bfc4b2e8..d29aea28ab510dd3757b21624e4c8d6d45daed52 100644
--- a/app/k9mail-jmap/build.gradle
+++ b/app/k9mail-jmap/build.gradle
@@ -36,7 +36,7 @@ dependencies {
testImplementation "com.google.truth:truth:${versions.truth}"
testImplementation "org.mockito:mockito-core:${versions.mockito}"
testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}"
- testImplementation "org.koin:koin-test:${versions.koin}"
+ testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}"
}
android {
diff --git a/app/k9mail-jmap/src/main/AndroidManifest.xml b/app/k9mail-jmap/src/main/AndroidManifest.xml
index 1270236c49a847412c56b7f83b2b8e174ab0f1d9..ec86eaf954d122000760d39dcf0e0821b426487b 100644
--- a/app/k9mail-jmap/src/main/AndroidManifest.xml
+++ b/app/k9mail-jmap/src/main/AndroidManifest.xml
@@ -1,6 +1,7 @@
@@ -31,7 +32,8 @@
android:label="@string/app_name"
android:theme="@style/Theme.K9.Startup"
android:resizeableActivity="true"
- android:allowBackup="false">
+ android:allowBackup="false"
+ tools:replace="android:theme">
-
-
-
-
-
-
-
-
-
diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/App.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/App.kt
index ef17a55f3a51651dee49a9d7d0d745f3d0400b0a..1a2c819baba78a4593e3e362ad00d06e28ef8f72 100644
--- a/app/k9mail-jmap/src/main/java/com/fsck/k9/App.kt
+++ b/app/k9mail-jmap/src/main/java/com/fsck/k9/App.kt
@@ -12,7 +12,7 @@ class App : Application() {
private val themeManager: ThemeManager by inject()
override fun onCreate() {
- Core.earlyInit(this)
+ Core.earlyInit()
super.onCreate()
diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt
index 574b92885f94d78996ee3ae0ed829df57f55da0f..0098b96e468d38b506cb0ea82f36c08c77af6d4b 100644
--- a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt
+++ b/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt
@@ -8,13 +8,13 @@ import com.fsck.k9.mail.ssl.TrustManagerFactory
import com.fsck.k9.mail.store.webdav.DraftsFolderProvider
import com.fsck.k9.mail.store.webdav.WebDavStore
import com.fsck.k9.mail.transport.WebDavTransport
-import com.fsck.k9.mailstore.FolderRepositoryManager
+import com.fsck.k9.mailstore.FolderRepository
import com.fsck.k9.mailstore.K9BackendStorageFactory
class WebDavBackendFactory(
private val backendStorageFactory: K9BackendStorageFactory,
private val trustManagerFactory: TrustManagerFactory,
- private val folderRepositoryManager: FolderRepositoryManager
+ private val folderRepository: FolderRepository
) : BackendFactory {
override fun createBackend(account: Account): Backend {
val accountName = account.displayName
@@ -27,10 +27,9 @@ class WebDavBackendFactory(
}
private fun createDraftsFolderProvider(account: Account): DraftsFolderProvider {
- val folderRepository = folderRepositoryManager.getFolderRepository(account)
return DraftsFolderProvider {
val draftsFolderId = account.draftsFolderId ?: error("No Drafts folder configured")
- folderRepository.getFolderServerId(draftsFolderId) ?: error("Couldn't find local Drafts folder")
+ folderRepository.getFolderServerId(account, draftsFolderId) ?: error("Couldn't find local Drafts folder")
}
}
}
diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.java b/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.java
deleted file mode 100644
index 8773ca22af2cc760cb4e0977cbb264fe9ed1d440..0000000000000000000000000000000000000000
--- a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.java
+++ /dev/null
@@ -1,232 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.List;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.DI;
-import com.fsck.k9.K9;
-import com.fsck.k9.activity.MessageList;
-import com.fsck.k9.ui.notification.DeleteConfirmationActivity;
-import com.fsck.k9.activity.compose.MessageActions;
-import com.fsck.k9.activity.setup.AccountSetupIncoming;
-import com.fsck.k9.activity.setup.AccountSetupOutgoing;
-import com.fsck.k9.controller.MessageReference;
-import com.fsck.k9.search.AccountSearchConditions;
-import com.fsck.k9.search.LocalSearch;
-import com.fsck.k9.ui.messagelist.DefaultFolderProvider;
-
-
-/**
- * This class contains methods to create the {@link PendingIntent}s for the actions of our notifications.
- *
- * Note:
- * We need to take special care to ensure the {@code PendingIntent}s are unique as defined in the documentation of
- * {@link PendingIntent}. Otherwise selecting a notification action might perform the action on the wrong message.
- *
- * We use the notification ID as {@code requestCode} argument to ensure each notification/action pair gets a unique
- * {@code PendingIntent}.
- */
-class K9NotificationActionCreator implements NotificationActionCreator {
- private final Context context;
- private final DefaultFolderProvider defaultFolderProvider = DI.get(DefaultFolderProvider.class);
-
-
- public K9NotificationActionCreator(Context context) {
- this.context = context;
- }
-
- @Override
- public PendingIntent createViewMessagePendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = createMessageViewIntent(messageReference);
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createViewFolderPendingIntent(Account account, long folderId, int notificationId) {
- Intent intent = createMessageListIntent(account, folderId);
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createViewMessagesPendingIntent(Account account, List messageReferences,
- int notificationId) {
-
- Long folderServerId = getFolderIdOfAllMessages(messageReferences);
-
- Intent intent;
- if (folderServerId == null) {
- intent = createMessageListIntent(account);
- } else {
- intent = createMessageListIntent(account, folderServerId);
- }
-
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createViewFolderListPendingIntent(Account account, int notificationId) {
- Intent intent = createMessageListIntent(account);
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createDismissAllMessagesPendingIntent(Account account, int notificationId) {
- Intent intent = NotificationActionService.createDismissAllMessagesIntent(context, account);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createDismissMessagePendingIntent(Context context, MessageReference messageReference,
- int notificationId) {
-
- Intent intent = NotificationActionService.createDismissMessageIntent(context, messageReference);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createReplyPendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = MessageActions.getActionReplyIntent(context, messageReference);
-
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createMarkMessageAsReadPendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createMarkAllAsReadPendingIntent(Account account, List messageReferences,
- int notificationId) {
- String accountUuid = account.getUuid();
- Intent intent = NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent getEditIncomingServerSettingsIntent(Account account) {
- Intent intent = AccountSetupIncoming.intentActionEditIncomingSettings(context, account);
-
- return PendingIntent.getActivity(context, account.getAccountNumber(), intent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent getEditOutgoingServerSettingsIntent(Account account) {
- Intent intent = AccountSetupOutgoing.intentActionEditOutgoingSettings(context, account);
-
- return PendingIntent.getActivity(context, account.getAccountNumber(), intent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createDeleteMessagePendingIntent(MessageReference messageReference, int notificationId) {
- if (K9.isConfirmDeleteFromNotification()) {
- return createDeleteConfirmationPendingIntent(messageReference, notificationId);
- } else {
- return createDeleteServicePendingIntent(messageReference, notificationId);
- }
- }
-
- private PendingIntent createDeleteServicePendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = NotificationActionService.createDeleteMessageIntent(context, messageReference);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- private PendingIntent createDeleteConfirmationPendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = DeleteConfirmationActivity.getIntent(context, messageReference);
-
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createDeleteAllPendingIntent(Account account, List messageReferences,
- int notificationId) {
- if (K9.isConfirmDeleteFromNotification()) {
- return getDeleteAllConfirmationPendingIntent(messageReferences, notificationId);
- } else {
- return getDeleteAllServicePendingIntent(account, messageReferences, notificationId);
- }
- }
-
- private PendingIntent getDeleteAllConfirmationPendingIntent(List messageReferences,
- int notificationId) {
- Intent intent = DeleteConfirmationActivity.getIntent(context, messageReferences);
-
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT);
- }
-
- private PendingIntent getDeleteAllServicePendingIntent(Account account, List messageReferences,
- int notificationId) {
- String accountUuid = account.getUuid();
- Intent intent = NotificationActionService.createDeleteAllMessagesIntent(
- context, accountUuid, messageReferences);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createArchiveMessagePendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = NotificationActionService.createArchiveMessageIntent(context, messageReference);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createArchiveAllPendingIntent(Account account, List messageReferences,
- int notificationId) {
- Intent intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createMarkMessageAsSpamPendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- private Intent createMessageListIntent(Account account) {
- long folderId = defaultFolderProvider.getDefaultFolder(account);
- LocalSearch search = new LocalSearch();
- search.addAllowedFolder(folderId);
- search.addAccountUuid(account.getUuid());
- return MessageList.intentDisplaySearch(context, search, false, true, true);
- }
-
- private Intent createMessageListIntent(Account account, long folderId) {
- LocalSearch search = new LocalSearch();
- search.addAllowedFolder(folderId);
- search.addAccountUuid(account.getUuid());
- return MessageList.intentDisplaySearch(context, search, false, true, true);
- }
-
- private Intent createMessageViewIntent(MessageReference message) {
- return MessageList.actionDisplayMessageIntent(context, message);
- }
-
- private Long getFolderIdOfAllMessages(List messageReferences) {
- MessageReference firstMessage = messageReferences.get(0);
- long folderId = firstMessage.getFolderId();
-
- for (MessageReference messageReference : messageReferences) {
- if (folderId != messageReference.getFolderId()) {
- return null;
- }
- }
-
- return folderId;
- }
-}
diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..33b59551f69c3eacc2074074612600aee24abc87
--- /dev/null
+++ b/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt
@@ -0,0 +1,234 @@
+package com.fsck.k9.notification
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import com.fsck.k9.Account
+import com.fsck.k9.K9
+import com.fsck.k9.activity.MessageList
+import com.fsck.k9.activity.compose.MessageActions
+import com.fsck.k9.activity.setup.AccountSetupIncoming
+import com.fsck.k9.activity.setup.AccountSetupOutgoing
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.search.LocalSearch
+import com.fsck.k9.ui.messagelist.DefaultFolderProvider
+import com.fsck.k9.ui.notification.DeleteConfirmationActivity
+
+/**
+ * This class contains methods to create the [PendingIntent]s for the actions of our notifications.
+ *
+ * **Note:**
+ * We need to take special care to ensure the `PendingIntent`s are unique as defined in the documentation of
+ * [PendingIntent]. Otherwise selecting a notification action might perform the action on the wrong message.
+ *
+ * We use the notification ID as `requestCode` argument to ensure each notification/action pair gets a unique
+ * `PendingIntent`.
+ */
+internal class K9NotificationActionCreator(
+ private val context: Context,
+ private val defaultFolderProvider: DefaultFolderProvider
+) : NotificationActionCreator {
+
+ override fun createViewMessagePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = createMessageViewIntent(messageReference)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createViewFolderPendingIntent(account: Account, folderId: Long, notificationId: Int): PendingIntent {
+ val intent = createMessageListIntent(account, folderId)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createViewMessagesPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ val folderServerId = getFolderIdOfAllMessages(messageReferences)
+ val intent = if (folderServerId != null) {
+ createMessageListIntent(account, folderServerId)
+ } else {
+ createMessageListIntent(account)
+ }
+
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createViewFolderListPendingIntent(account: Account, notificationId: Int): PendingIntent {
+ val intent = createMessageListIntent(account)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createDismissAllMessagesPendingIntent(account: Account, notificationId: Int): PendingIntent {
+ val intent = NotificationActionService.createDismissAllMessagesIntent(context, account)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createDismissMessagePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createDismissMessageIntent(context, messageReference)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createReplyPendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent {
+ val intent = MessageActions.getActionReplyIntent(context, messageReference)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createMarkMessageAsReadPendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createMarkAllAsReadPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ val accountUuid = account.uuid
+ val intent = NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun getEditIncomingServerSettingsIntent(account: Account): PendingIntent {
+ val intent = AccountSetupIncoming.intentActionEditIncomingSettings(context, account)
+ return PendingIntent.getActivity(context, account.accountNumber, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun getEditOutgoingServerSettingsIntent(account: Account): PendingIntent {
+ val intent = AccountSetupOutgoing.intentActionEditOutgoingSettings(context, account)
+ return PendingIntent.getActivity(context, account.accountNumber, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createDeleteMessagePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ return if (K9.isConfirmDeleteFromNotification) {
+ createDeleteConfirmationPendingIntent(messageReference, notificationId)
+ } else {
+ createDeleteServicePendingIntent(messageReference, notificationId)
+ }
+ }
+
+ private fun createDeleteServicePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createDeleteMessageIntent(context, messageReference)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ private fun createDeleteConfirmationPendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = DeleteConfirmationActivity.getIntent(context, messageReference)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createDeleteAllPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ return if (K9.isConfirmDeleteFromNotification) {
+ getDeleteAllConfirmationPendingIntent(messageReferences, notificationId)
+ } else {
+ getDeleteAllServicePendingIntent(account, messageReferences, notificationId)
+ }
+ }
+
+ private fun getDeleteAllConfirmationPendingIntent(
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = DeleteConfirmationActivity.getIntent(context, messageReferences)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT)
+ }
+
+ private fun getDeleteAllServicePendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ val accountUuid = account.uuid
+ val intent = NotificationActionService.createDeleteAllMessagesIntent(context, accountUuid, messageReferences)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createArchiveMessagePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createArchiveMessageIntent(context, messageReference)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createArchiveAllPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createMarkMessageAsSpamPendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ private fun createMessageListIntent(account: Account): Intent {
+ val folderId = defaultFolderProvider.getDefaultFolder(account)
+ val search = LocalSearch().apply {
+ addAllowedFolder(folderId)
+ addAccountUuid(account.uuid)
+ }
+
+ return MessageList.intentDisplaySearch(
+ context = context,
+ search = search,
+ noThreading = false,
+ newTask = true,
+ clearTop = true
+ )
+ }
+
+ private fun createMessageListIntent(account: Account, folderId: Long): Intent {
+ val search = LocalSearch().apply {
+ addAllowedFolder(folderId)
+ addAccountUuid(account.uuid)
+ }
+
+ return MessageList.intentDisplaySearch(
+ context = context,
+ search = search,
+ noThreading = false,
+ newTask = true,
+ clearTop = true
+ )
+ }
+
+ private fun createMessageViewIntent(message: MessageReference): Intent {
+ return MessageList.actionDisplayMessageIntent(context, message)
+ }
+
+ private fun getFolderIdOfAllMessages(messageReferences: List): Long? {
+ val firstMessage = messageReferences.first()
+ val folderId = firstMessage.folderId
+
+ return if (messageReferences.all { it.folderId == folderId }) folderId else null
+ }
+}
diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationResourceProvider.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationResourceProvider.kt
index 839ce99d9547bfb691e996adf9f1649ff8eb7254..0cea5e4c1112547859725afd900dbf15a9803be0 100644
--- a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationResourceProvider.kt
+++ b/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationResourceProvider.kt
@@ -36,6 +36,8 @@ class K9NotificationResourceProvider(private val context: Context) : Notificatio
override fun authenticationErrorBody(accountName: String): String =
context.getString(R.string.notification_authentication_error_text, accountName)
+ override fun certificateErrorTitle(): String = context.getString(R.string.notification_certificate_error_public)
+
override fun certificateErrorTitle(accountName: String): String =
context.getString(R.string.notification_certificate_error_title, accountName)
diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/KoinModule.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/KoinModule.kt
index ce46fad3157de2a7b508da28ee8eace8c62722fa..ac5ee21308f2bdaf0674ce923cc6f055dfc67b8b 100644
--- a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/KoinModule.kt
+++ b/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/KoinModule.kt
@@ -3,7 +3,7 @@ package com.fsck.k9.notification
import org.koin.dsl.module
val notificationModule = module {
- single { K9NotificationActionCreator(get()) }
+ single { K9NotificationActionCreator(context = get(), defaultFolderProvider = get()) }
single { K9NotificationResourceProvider(get()) }
single { K9NotificationStrategy(get()) }
}
diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/resources/K9CoreResourceProvider.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/resources/K9CoreResourceProvider.kt
index 8f2f61fa2ab55d5abf4fa5ff7634ca1655825575..6c46f88f2dd11e5f409e50869e6782b8775cbca6 100644
--- a/app/k9mail-jmap/src/main/java/com/fsck/k9/resources/K9CoreResourceProvider.kt
+++ b/app/k9mail-jmap/src/main/java/com/fsck/k9/resources/K9CoreResourceProvider.kt
@@ -9,12 +9,6 @@ class K9CoreResourceProvider(private val context: Context) : CoreResourceProvide
override fun defaultSignature(): String = context.getString(R.string.default_signature)
override fun defaultIdentityDescription(): String = context.getString(R.string.default_identity_description)
- override fun internalStorageProviderName(): String =
- context.getString(R.string.local_storage_provider_internal_label)
-
- override fun externalStorageProviderName(): String =
- context.getString(R.string.local_storage_provider_external_label)
-
override fun contactDisplayNamePrefix(): String = context.getString(R.string.message_to_label)
override fun contactUnknownSender(): String = context.getString(R.string.unknown_sender)
override fun contactUnknownRecipient(): String = context.getString(R.string.unknown_recipient)
@@ -37,8 +31,6 @@ class K9CoreResourceProvider(private val context: Context) : CoreResourceProvide
override fun replyHeader(sender: String, sentDate: String): String =
context.getString(R.string.message_compose_reply_header_fmt_with_date, sentDate, sender)
- override fun searchAllMessagesTitle(): String = context.getString(R.string.search_all_messages_title)
- override fun searchAllMessagesDetail(): String = context.getString(R.string.search_all_messages_detail)
override fun searchUnifiedInboxTitle(): String = context.getString(R.string.integrated_inbox_title)
override fun searchUnifiedInboxDetail(): String = context.getString(R.string.integrated_inbox_detail)
diff --git a/app/k9mail-jmap/src/main/res/values/themes.xml b/app/k9mail-jmap/src/main/res/values/themes.xml
index 4d8dff3ded1dd9bd48fa0bd0e4cf70c132e704ef..700b046c83e1ea06bdcda35a463f0209ef1f4e38 100644
--- a/app/k9mail-jmap/src/main/res/values/themes.xml
+++ b/app/k9mail-jmap/src/main/res/values/themes.xml
@@ -46,13 +46,14 @@
- @drawable/ic_star
- @drawable/ic_star_border
- @drawable/ic_opened_envelope
- - @drawable/ic_envelope
+ - @drawable/ic_mark_new
- @drawable/ic_magnify_cloud
- @drawable/ic_plus
- @drawable/ic_arrow_up_down
- @drawable/ic_file_upload
- @drawable/ic_select_all
- @drawable/ic_floppy
+ - @drawable/ic_download
- @drawable/ic_clear
- @drawable/ic_action_request_read_receipt_light
- @drawable/ic_chevron_down
@@ -92,6 +93,7 @@
- @drawable/ic_messagelist_forwarded
- @drawable/ic_messagelist_answered_forwarded
- @drawable/btn_check_star
+ - #fbbc04
- #ffffffff
- @drawable/ic_person_plus
- #e8e8e8
@@ -163,13 +165,14 @@
- @drawable/ic_star
- @drawable/ic_star_border
- @drawable/ic_opened_envelope
- - @drawable/ic_envelope
+ - @drawable/ic_mark_new
- @drawable/ic_magnify_cloud
- @drawable/ic_plus
- @drawable/ic_arrow_up_down
- @drawable/ic_file_upload
- @drawable/ic_select_all
- @drawable/ic_floppy
+ - @drawable/ic_download
- @drawable/ic_clear
- @drawable/ic_action_request_read_receipt_dark
- @drawable/ic_chevron_down
@@ -209,6 +212,7 @@
- @drawable/ic_messagelist_forwarded
- @drawable/ic_messagelist_answered_forwarded
- @drawable/btn_check_star
+ - #fdd663
- #000000
- @drawable/ic_person_plus
- #313131
diff --git a/app/k9mail-jmap/src/test/java/com/fsck/k9/DependencyInjectionTest.kt b/app/k9mail-jmap/src/test/java/com/fsck/k9/DependencyInjectionTest.kt
index 880498b6405b33cf5f0c0f8a40e5a7557d88417a..14a763039c75cd0067379e4be481cc5b8c52178f 100644
--- a/app/k9mail-jmap/src/test/java/com/fsck/k9/DependencyInjectionTest.kt
+++ b/app/k9mail-jmap/src/test/java/com/fsck/k9/DependencyInjectionTest.kt
@@ -10,7 +10,7 @@ import com.fsck.k9.ui.folders.FolderNameFormatter
import com.fsck.k9.ui.helper.SizeFormatter
import org.junit.Test
import org.junit.runner.RunWith
-import org.koin.core.annotation.KoinInternal
+import org.koin.core.annotation.KoinInternalApi
import org.koin.core.logger.PrintLogger
import org.koin.core.parameter.parametersOf
import org.koin.java.KoinJavaComponent
@@ -31,17 +31,17 @@ class DependencyInjectionTest : AutoCloseKoinTest() {
}
val autocryptTransferView = mock()
- @KoinInternal
+ @KoinInternalApi
@Test
fun testDependencyTree() {
KoinJavaComponent.getKoin().setupLogger(PrintLogger())
getKoin().checkModules {
- create { parametersOf(lifecycleOwner) }
+ withParameter { lifecycleOwner }
create { parametersOf(lifecycleOwner, autocryptTransferView) }
- create { parametersOf(RuntimeEnvironment.application) }
- create { parametersOf(RuntimeEnvironment.application) }
- create { parametersOf(ChangeLogMode.CHANGE_LOG) }
+ withParameter { RuntimeEnvironment.application }
+ withParameter { RuntimeEnvironment.application }
+ withParameter { ChangeLogMode.CHANGE_LOG }
}
}
}
diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle
index d76a16f157b00a851e5d9c17586917d21106d1fc..01cb3adb016ab2a9539beafb1d737cb50a632679 100644
--- a/app/k9mail/build.gradle
+++ b/app/k9mail/build.gradle
@@ -14,6 +14,7 @@ dependencies {
implementation project(":backend:imap")
implementation project(":backend:pop3")
implementation project(":backend:webdav")
+ debugImplementation project(":backend:demo")
implementation "androidx.appcompat:appcompat:${versions.androidxAppCompat}"
implementation "androidx.core:core-ktx:${versions.androidxCore}"
@@ -36,7 +37,7 @@ dependencies {
testImplementation "com.google.truth:truth:${versions.truth}"
testImplementation "org.mockito:mockito-core:${versions.mockito}"
testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}"
- testImplementation "org.koin:koin-test:${versions.koin}"
+ testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}"
}
android {
@@ -47,8 +48,8 @@ android {
applicationId "foundation.e.mail"
testApplicationId "foundation.e.mail.tests"
- versionCode 28000
- versionName '5.800'
+ versionCode 29008
+ versionName '5.909-SNAPSHOT'
// Keep in sync with the resource string array 'supported_languages'
resConfigs "in", "br", "ca", "cs", "cy", "da", "de", "et", "en", "en_GB", "es", "eo", "eu", "fr", "gd", "gl",
@@ -89,9 +90,12 @@ android {
}
}
- // Do not abort build if lint finds errors
lintOptions {
+ checkDependencies true
+
+ // Do not abort build if lint finds errors
abortOnError false
+
lintConfig file("$rootProject.projectDir/config/lint/lint.xml")
}
diff --git a/app/k9mail/src/debug/java/app/k9mail/dev/DebugConfig.kt b/app/k9mail/src/debug/java/app/k9mail/dev/DebugConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6d4b73b16bdbc3c4814960f50e22b29470e1af84
--- /dev/null
+++ b/app/k9mail/src/debug/java/app/k9mail/dev/DebugConfig.kt
@@ -0,0 +1,12 @@
+package app.k9mail.dev
+
+import org.koin.core.module.Module
+import org.koin.core.scope.Scope
+
+fun Scope.developmentBackends() = mapOf(
+ "demo" to get()
+)
+
+fun Module.developmentModuleAdditions() {
+ single { DemoBackendFactory(backendStorageFactory = get()) }
+}
diff --git a/app/k9mail/src/debug/java/app/k9mail/dev/DemoBackendFactory.kt b/app/k9mail/src/debug/java/app/k9mail/dev/DemoBackendFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..366fdc61953b8b6d38c591a63ad21c1baf392d82
--- /dev/null
+++ b/app/k9mail/src/debug/java/app/k9mail/dev/DemoBackendFactory.kt
@@ -0,0 +1,14 @@
+package app.k9mail.dev
+
+import app.k9mail.backend.demo.DemoBackend
+import com.fsck.k9.Account
+import com.fsck.k9.backend.BackendFactory
+import com.fsck.k9.backend.api.Backend
+import com.fsck.k9.mailstore.K9BackendStorageFactory
+
+class DemoBackendFactory(private val backendStorageFactory: K9BackendStorageFactory) : BackendFactory {
+ override fun createBackend(account: Account): Backend {
+ val backendStorage = backendStorageFactory.createBackendStorage(account)
+ return DemoBackend(backendStorage)
+ }
+}
diff --git a/app/k9mail/src/debug/res/values/app-specific.xml b/app/k9mail/src/debug/res/values/app-specific.xml
deleted file mode 100644
index 3bc22ec0b41154fa6a091f45e747ff1fa1a7f8ff..0000000000000000000000000000000000000000
--- a/app/k9mail/src/debug/res/values/app-specific.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- com.fsck.k9.debug
-
diff --git a/app/k9mail/src/main/AndroidManifest.xml b/app/k9mail/src/main/AndroidManifest.xml
index 30e30c7025d9834e84ce63ba8c493d0dded8a492..85e38be8ddf6c7f51957ae78afbdcc45e37587e6 100644
--- a/app/k9mail/src/main/AndroidManifest.xml
+++ b/app/k9mail/src/main/AndroidManifest.xml
@@ -1,6 +1,7 @@
@@ -45,12 +46,17 @@
android:name="com.fsck.k9.App"
android:allowTaskReparenting="false"
android:usesCleartextTraffic="true"
+ android:networkSecurityConfig="@xml/network_security_config"
+
android:icon="@drawable/ic_e_launcher"
android:roundIcon="@mipmap/icon_e"
android:label="@string/app_name"
android:theme="@style/Theme.K9.Startup"
android:resizeableActivity="true"
- android:allowBackup="false">
+ android:allowBackup="false"
+ android:supportsRtl="true"
+ tools:replace="android:theme"
+ tools:ignore="UnusedAttribute">
+
+
-
+
@@ -199,6 +212,7 @@
android:name=".activity.MessageCompose"
android:configChanges="locale"
android:enabled="false"
+ android:exported="true"
android:label="@string/app_name">
@@ -237,7 +251,8 @@
android:name=".activity.Search"
android:configChanges="locale"
android:label="@string/search_action"
- android:uiOptions="splitActionBarWhenNarrow">
+ android:uiOptions="splitActionBarWhenNarrow"
+ android:exported="true">
@@ -250,14 +265,17 @@
+ android:label="@string/shortcuts_title"
+ android:exported="true">
-
+
@@ -342,7 +360,8 @@
+ android:label="@string/mail_list_widget_text"
+ android:exported="true">
@@ -376,7 +395,8 @@
+ android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE"
+ android:exported="true">
diff --git a/app/k9mail/src/main/java/com/fsck/k9/App.kt b/app/k9mail/src/main/java/com/fsck/k9/App.kt
index 79646b9fe14f1206a3155ae0fd43a112d90f2b25..68991190e99caab3e54bce25b5d9c1ca037e3f6e 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/App.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/App.kt
@@ -29,7 +29,7 @@ class App : Application() {
private var appLanguageManagerInitialized = false
override fun onCreate() {
- Core.earlyInit(this)
+ Core.earlyInit()
super.onCreate()
diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt
index bc2269051ef4726bfcfa77c3a0a93203584a41c2..1b947b8ded21e02b921ba3305c4967a5552a0914 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt
@@ -1,5 +1,7 @@
package com.fsck.k9.backends
+import app.k9mail.dev.developmentBackends
+import app.k9mail.dev.developmentModuleAdditions
import com.fsck.k9.backend.BackendManager
import com.fsck.k9.backend.imap.BackendIdleRefreshManager
import com.fsck.k9.backend.imap.SystemAlarmManager
@@ -13,7 +15,7 @@ val backendsModule = module {
"imap" to get(),
"pop3" to get(),
"webdav" to get()
- )
+ ) + developmentBackends()
)
}
single {
@@ -31,4 +33,6 @@ val backendsModule = module {
single { BackendIdleRefreshManager(alarmManager = get()) }
single { Pop3BackendFactory(get(), get()) }
single { WebDavBackendFactory(get(), get(), get()) }
+
+ developmentModuleAdditions()
}
diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt
index 574b92885f94d78996ee3ae0ed829df57f55da0f..0098b96e468d38b506cb0ea82f36c08c77af6d4b 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt
@@ -8,13 +8,13 @@ import com.fsck.k9.mail.ssl.TrustManagerFactory
import com.fsck.k9.mail.store.webdav.DraftsFolderProvider
import com.fsck.k9.mail.store.webdav.WebDavStore
import com.fsck.k9.mail.transport.WebDavTransport
-import com.fsck.k9.mailstore.FolderRepositoryManager
+import com.fsck.k9.mailstore.FolderRepository
import com.fsck.k9.mailstore.K9BackendStorageFactory
class WebDavBackendFactory(
private val backendStorageFactory: K9BackendStorageFactory,
private val trustManagerFactory: TrustManagerFactory,
- private val folderRepositoryManager: FolderRepositoryManager
+ private val folderRepository: FolderRepository
) : BackendFactory {
override fun createBackend(account: Account): Backend {
val accountName = account.displayName
@@ -27,10 +27,9 @@ class WebDavBackendFactory(
}
private fun createDraftsFolderProvider(account: Account): DraftsFolderProvider {
- val folderRepository = folderRepositoryManager.getFolderRepository(account)
return DraftsFolderProvider {
val draftsFolderId = account.draftsFolderId ?: error("No Drafts folder configured")
- folderRepository.getFolderServerId(draftsFolderId) ?: error("Couldn't find local Drafts folder")
+ folderRepository.getFolderServerId(account, draftsFolderId) ?: error("Couldn't find local Drafts folder")
}
}
}
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 d3929ff5a16019cd2e502fc75414e1fa79079b7c..97fa09796f8799eeab43b3af274b0b7fb618f456 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
@@ -152,10 +152,6 @@ public class MessageProvider extends ContentProvider {
for (Account account : Preferences.getPreferences(getContext()).getAccounts()) {
if (account.getAccountNumber() == accountId) {
myAccount = account;
- if (!account.isAvailable(getContext())) {
- Timber.w("not deleting messages because account is unavailable at the moment");
- return 0;
- }
}
}
@@ -164,7 +160,7 @@ public class MessageProvider extends ContentProvider {
}
if (myAccount != null) {
- MessageReference messageReference = new MessageReference(myAccount.getUuid(), folderId, msgUid, null);
+ MessageReference messageReference = new MessageReference(myAccount.getUuid(), folderId, msgUid);
MessagingController controller = MessagingController.getInstance(getContext());
controller.deleteMessage(messageReference);
}
@@ -661,7 +657,7 @@ public class MessageProvider extends ContentProvider {
Context context = getContext();
MessagingController controller = MessagingController.getInstance(context);
- Collection accounts = Preferences.getPreferences(context).getAvailableAccounts();
+ Collection accounts = Preferences.getPreferences(context).getAccounts();
for (Account account : accounts) {
if (account.getAccountNumber() == accountNumber) {
diff --git a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.java b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.java
deleted file mode 100644
index f0d8b471c820290ed17d10682d56a578e064640a..0000000000000000000000000000000000000000
--- a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.java
+++ /dev/null
@@ -1,232 +0,0 @@
-package com.fsck.k9.notification;
-
-
-import java.util.List;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.DI;
-import com.fsck.k9.K9;
-import com.fsck.k9.activity.MessageList;
-import com.fsck.k9.controller.MessageReference;
-import com.fsck.k9.ui.notification.DeleteConfirmationActivity;
-import com.fsck.k9.activity.compose.MessageActions;
-import com.fsck.k9.activity.setup.AccountSetupIncoming;
-import com.fsck.k9.activity.setup.AccountSetupOutgoing;
-import com.fsck.k9.search.AccountSearchConditions;
-import com.fsck.k9.search.LocalSearch;
-import com.fsck.k9.ui.messagelist.DefaultFolderProvider;
-
-
-/**
- * This class contains methods to create the {@link PendingIntent}s for the actions of our notifications.
- *
- * Note:
- * We need to take special care to ensure the {@code PendingIntent}s are unique as defined in the documentation of
- * {@link PendingIntent}. Otherwise selecting a notification action might perform the action on the wrong message.
- *
- * We use the notification ID as {@code requestCode} argument to ensure each notification/action pair gets a unique
- * {@code PendingIntent}.
- */
-class K9NotificationActionCreator implements NotificationActionCreator {
- private final Context context;
- private final DefaultFolderProvider defaultFolderProvider = DI.get(DefaultFolderProvider.class);
-
-
- public K9NotificationActionCreator(Context context) {
- this.context = context;
- }
-
- @Override
- public PendingIntent createViewMessagePendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = createMessageViewIntent(messageReference);
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createViewFolderPendingIntent(Account account, long folderId, int notificationId) {
- Intent intent = createMessageListIntent(account, folderId);
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createViewMessagesPendingIntent(Account account, List messageReferences,
- int notificationId) {
-
- Long folderServerId = getFolderIdOfAllMessages(messageReferences);
-
- Intent intent;
- if (folderServerId == null) {
- intent = createMessageListIntent(account);
- } else {
- intent = createMessageListIntent(account, folderServerId);
- }
-
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createViewFolderListPendingIntent(Account account, int notificationId) {
- Intent intent = createMessageListIntent(account);
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createDismissAllMessagesPendingIntent(Account account, int notificationId) {
- Intent intent = NotificationActionService.createDismissAllMessagesIntent(context, account);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createDismissMessagePendingIntent(Context context, MessageReference messageReference,
- int notificationId) {
-
- Intent intent = NotificationActionService.createDismissMessageIntent(context, messageReference);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createReplyPendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = MessageActions.getActionReplyIntent(context, messageReference);
-
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createMarkMessageAsReadPendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createMarkAllAsReadPendingIntent(Account account, List messageReferences,
- int notificationId) {
- String accountUuid = account.getUuid();
- Intent intent = NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent getEditIncomingServerSettingsIntent(Account account) {
- Intent intent = AccountSetupIncoming.intentActionEditIncomingSettings(context, account);
-
- return PendingIntent.getActivity(context, account.getAccountNumber(), intent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent getEditOutgoingServerSettingsIntent(Account account) {
- Intent intent = AccountSetupOutgoing.intentActionEditOutgoingSettings(context, account);
-
- return PendingIntent.getActivity(context, account.getAccountNumber(), intent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createDeleteMessagePendingIntent(MessageReference messageReference, int notificationId) {
- if (K9.isConfirmDeleteFromNotification()) {
- return createDeleteConfirmationPendingIntent(messageReference, notificationId);
- } else {
- return createDeleteServicePendingIntent(messageReference, notificationId);
- }
- }
-
- private PendingIntent createDeleteServicePendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = NotificationActionService.createDeleteMessageIntent(context, messageReference);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- private PendingIntent createDeleteConfirmationPendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = DeleteConfirmationActivity.getIntent(context, messageReference);
-
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createDeleteAllPendingIntent(Account account, List messageReferences,
- int notificationId) {
- if (K9.isConfirmDeleteFromNotification()) {
- return getDeleteAllConfirmationPendingIntent(messageReferences, notificationId);
- } else {
- return getDeleteAllServicePendingIntent(account, messageReferences, notificationId);
- }
- }
-
- private PendingIntent getDeleteAllConfirmationPendingIntent(List messageReferences,
- int notificationId) {
- Intent intent = DeleteConfirmationActivity.getIntent(context, messageReferences);
-
- return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT);
- }
-
- private PendingIntent getDeleteAllServicePendingIntent(Account account, List messageReferences,
- int notificationId) {
- String accountUuid = account.getUuid();
- Intent intent = NotificationActionService.createDeleteAllMessagesIntent(
- context, accountUuid, messageReferences);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createArchiveMessagePendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = NotificationActionService.createArchiveMessageIntent(context, messageReference);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createArchiveAllPendingIntent(Account account, List messageReferences,
- int notificationId) {
- Intent intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- @Override
- public PendingIntent createMarkMessageAsSpamPendingIntent(MessageReference messageReference, int notificationId) {
- Intent intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference);
-
- return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- private Intent createMessageListIntent(Account account) {
- long folderId = defaultFolderProvider.getDefaultFolder(account);
- LocalSearch search = new LocalSearch();
- search.addAllowedFolder(folderId);
- search.addAccountUuid(account.getUuid());
- return MessageList.intentDisplaySearch(context, search, false, true, true);
- }
-
- private Intent createMessageListIntent(Account account, long folderId) {
- LocalSearch search = new LocalSearch();
- search.addAllowedFolder(folderId);
- search.addAccountUuid(account.getUuid());
- return MessageList.intentDisplaySearch(context, search, false, true, true);
- }
-
- private Intent createMessageViewIntent(MessageReference message) {
- return MessageList.actionDisplayMessageIntent(context, message);
- }
-
- private Long getFolderIdOfAllMessages(List messageReferences) {
- MessageReference firstMessage = messageReferences.get(0);
- long folderId = firstMessage.getFolderId();
-
- for (MessageReference messageReference : messageReferences) {
- if (folderId != messageReference.getFolderId()) {
- return null;
- }
- }
-
- return folderId;
- }
-}
diff --git a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d03ca29a489b37dfb785c4c0307e8394380f95b5
--- /dev/null
+++ b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt
@@ -0,0 +1,256 @@
+package com.fsck.k9.notification
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import com.fsck.k9.Account
+import com.fsck.k9.K9
+import com.fsck.k9.activity.MessageList
+import com.fsck.k9.activity.compose.MessageActions
+import com.fsck.k9.activity.setup.AccountSetupIncoming
+import com.fsck.k9.activity.setup.AccountSetupOutgoing
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.mailstore.MessageStoreManager
+import com.fsck.k9.search.LocalSearch
+import com.fsck.k9.ui.messagelist.DefaultFolderProvider
+import com.fsck.k9.ui.notification.DeleteConfirmationActivity
+
+/**
+ * This class contains methods to create the [PendingIntent]s for the actions of our notifications.
+ *
+ * **Note:**
+ * We need to take special care to ensure the `PendingIntent`s are unique as defined in the documentation of
+ * [PendingIntent]. Otherwise selecting a notification action might perform the action on the wrong message.
+ *
+ * We use the notification ID as `requestCode` argument to ensure each notification/action pair gets a unique
+ * `PendingIntent`.
+ */
+internal class K9NotificationActionCreator(
+ private val context: Context,
+ private val defaultFolderProvider: DefaultFolderProvider,
+ private val messageStoreManager: MessageStoreManager
+) : NotificationActionCreator {
+
+ override fun createViewMessagePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val openInUnifiedInbox = K9.isShowUnifiedInbox && isIncludedInUnifiedInbox(messageReference)
+ val intent = createMessageViewIntent(messageReference, openInUnifiedInbox)
+
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createViewFolderPendingIntent(account: Account, folderId: Long, notificationId: Int): PendingIntent {
+ val intent = createMessageListIntent(account, folderId)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createViewMessagesPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ val folderIds = extractFolderIds(messageReferences)
+
+ val intent = if (K9.isShowUnifiedInbox && areAllIncludedInUnifiedInbox(account, folderIds)) {
+ createUnifiedInboxIntent(account)
+ } else if (folderIds.size == 1) {
+ createMessageListIntent(account, folderIds.first())
+ } else {
+ createNewMessagesIntent(account)
+ }
+
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createViewFolderListPendingIntent(account: Account, notificationId: Int): PendingIntent {
+ val intent = createMessageListIntent(account)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createDismissAllMessagesPendingIntent(account: Account, notificationId: Int): PendingIntent {
+ val intent = NotificationActionService.createDismissAllMessagesIntent(context, account)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createDismissMessagePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createDismissMessageIntent(context, messageReference)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createReplyPendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent {
+ val intent = MessageActions.getActionReplyIntent(context, messageReference)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createMarkMessageAsReadPendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createMarkAllAsReadPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ val accountUuid = account.uuid
+ val intent = NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun getEditIncomingServerSettingsIntent(account: Account): PendingIntent {
+ val intent = AccountSetupIncoming.intentActionEditIncomingSettings(context, account)
+ return PendingIntent.getActivity(context, account.accountNumber, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun getEditOutgoingServerSettingsIntent(account: Account): PendingIntent {
+ val intent = AccountSetupOutgoing.intentActionEditOutgoingSettings(context, account)
+ return PendingIntent.getActivity(context, account.accountNumber, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createDeleteMessagePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ return if (K9.isConfirmDeleteFromNotification) {
+ createDeleteConfirmationPendingIntent(messageReference, notificationId)
+ } else {
+ createDeleteServicePendingIntent(messageReference, notificationId)
+ }
+ }
+
+ private fun createDeleteServicePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createDeleteMessageIntent(context, messageReference)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ private fun createDeleteConfirmationPendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = DeleteConfirmationActivity.getIntent(context, messageReference)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createDeleteAllPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ return if (K9.isConfirmDeleteFromNotification) {
+ getDeleteAllConfirmationPendingIntent(messageReferences, notificationId)
+ } else {
+ getDeleteAllServicePendingIntent(account, messageReferences, notificationId)
+ }
+ }
+
+ private fun getDeleteAllConfirmationPendingIntent(
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = DeleteConfirmationActivity.getIntent(context, messageReferences)
+ return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT)
+ }
+
+ private fun getDeleteAllServicePendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ val accountUuid = account.uuid
+ val intent = NotificationActionService.createDeleteAllMessagesIntent(context, accountUuid, messageReferences)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createArchiveMessagePendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createArchiveMessageIntent(context, messageReference)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createArchiveAllPendingIntent(
+ account: Account,
+ messageReferences: List,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ override fun createMarkMessageAsSpamPendingIntent(
+ messageReference: MessageReference,
+ notificationId: Int
+ ): PendingIntent {
+ val intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference)
+ return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ private fun createMessageListIntent(account: Account): Intent {
+ val folderId = defaultFolderProvider.getDefaultFolder(account)
+ val search = LocalSearch().apply {
+ addAllowedFolder(folderId)
+ addAccountUuid(account.uuid)
+ }
+
+ return MessageList.intentDisplaySearch(
+ context = context,
+ search = search,
+ noThreading = false,
+ newTask = true,
+ clearTop = true
+ )
+ }
+
+ private fun createMessageListIntent(account: Account, folderId: Long): Intent {
+ val search = LocalSearch().apply {
+ addAllowedFolder(folderId)
+ addAccountUuid(account.uuid)
+ }
+
+ return MessageList.intentDisplaySearch(
+ context = context,
+ search = search,
+ noThreading = false,
+ newTask = true,
+ clearTop = true
+ )
+ }
+
+ private fun createMessageViewIntent(message: MessageReference, openInUnifiedInbox: Boolean): Intent {
+ return MessageList.actionDisplayMessageIntent(context, message, openInUnifiedInbox)
+ }
+
+ private fun createUnifiedInboxIntent(account: Account): Intent {
+ return MessageList.createUnifiedInboxIntent(context, account)
+ }
+
+ private fun createNewMessagesIntent(account: Account): Intent {
+ return MessageList.createNewMessagesIntent(context, account)
+ }
+
+ private fun extractFolderIds(messageReferences: List): Set {
+ return messageReferences.asSequence().map { it.folderId }.toSet()
+ }
+
+ private fun areAllIncludedInUnifiedInbox(account: Account, folderIds: Collection): Boolean {
+ val messageStore = messageStoreManager.getMessageStore(account)
+ return messageStore.areAllIncludedInUnifiedInbox(folderIds)
+ }
+
+ private fun isIncludedInUnifiedInbox(messageReference: MessageReference): Boolean {
+ val messageStore = messageStoreManager.getMessageStore(messageReference.accountUuid)
+ return messageStore.areAllIncludedInUnifiedInbox(listOf(messageReference.folderId))
+ }
+}
diff --git a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationResourceProvider.kt b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationResourceProvider.kt
index cb7b2a4455b82bcfdd143824e3f8d011ff2a94f5..5304232375691c51aea6ec314d9515c96231b627 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationResourceProvider.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationResourceProvider.kt
@@ -36,6 +36,8 @@ class K9NotificationResourceProvider(private val context: Context) : Notificatio
override fun authenticationErrorBody(accountName: String): String =
context.getString(R.string.notification_authentication_error_text, accountName)
+ override fun certificateErrorTitle(): String = context.getString(R.string.notification_certificate_error_public)
+
override fun certificateErrorTitle(accountName: String): String =
context.getString(R.string.notification_certificate_error_title, accountName)
diff --git a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt
index 254c4abe7b44c4327f43f2cefac553a67f95b985..37630d5e9d7ce53d36d3d8886f5353d097bee02b 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt
@@ -4,6 +4,8 @@ import com.fsck.k9.Account
import com.fsck.k9.K9
import com.fsck.k9.helper.Contacts
import com.fsck.k9.mail.Flag
+import com.fsck.k9.mail.K9MailLib
+import com.fsck.k9.mail.Message
import com.fsck.k9.mailstore.LocalFolder
import com.fsck.k9.mailstore.LocalFolder.isModeMismatch
import com.fsck.k9.mailstore.LocalMessage
@@ -80,6 +82,11 @@ class K9NotificationStrategy(private val contacts: Contacts) : NotificationStrat
return false
}
+ if (account.isIgnoreChatMessages && message.isChatMessage) {
+ Timber.v("No notification: Notifications for chat messages are disabled")
+ return false
+ }
+
if (!account.isNotifySelfNewMail && account.isAnIdentity(message.from)) {
Timber.v("No notification: Notifications for messages from yourself are disabled")
return false
@@ -92,4 +99,7 @@ class K9NotificationStrategy(private val contacts: Contacts) : NotificationStrat
return true
}
+
+ private val Message.isChatMessage: Boolean
+ get() = getHeader(K9MailLib.CHAT_HEADER).isNotEmpty()
}
diff --git a/app/k9mail/src/main/java/com/fsck/k9/notification/KoinModule.kt b/app/k9mail/src/main/java/com/fsck/k9/notification/KoinModule.kt
index ce46fad3157de2a7b508da28ee8eace8c62722fa..d86c64d8a04d22e28b1c814953833a65807a4cd5 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/notification/KoinModule.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/notification/KoinModule.kt
@@ -3,7 +3,9 @@ package com.fsck.k9.notification
import org.koin.dsl.module
val notificationModule = module {
- single { K9NotificationActionCreator(get()) }
+ single {
+ K9NotificationActionCreator(context = get(), defaultFolderProvider = get(), messageStoreManager = get())
+ }
single { K9NotificationResourceProvider(get()) }
single { K9NotificationStrategy(get()) }
}
diff --git a/app/k9mail/src/main/java/com/fsck/k9/resources/K9CoreResourceProvider.kt b/app/k9mail/src/main/java/com/fsck/k9/resources/K9CoreResourceProvider.kt
index cd2603adbeab4494bb0195ada5097230f40803f3..270736c3ee3de5be8647f454d8226076baf14974 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/resources/K9CoreResourceProvider.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/resources/K9CoreResourceProvider.kt
@@ -9,12 +9,6 @@ class K9CoreResourceProvider(private val context: Context) : CoreResourceProvide
override fun defaultSignature(): String = context.getString(R.string.default_signature)
override fun defaultIdentityDescription(): String = context.getString(R.string.default_identity_description)
- override fun internalStorageProviderName(): String =
- context.getString(R.string.local_storage_provider_internal_label)
-
- override fun externalStorageProviderName(): String =
- context.getString(R.string.local_storage_provider_external_label)
-
override fun contactDisplayNamePrefix(): String = context.getString(R.string.message_to_label)
override fun contactUnknownSender(): String = context.getString(R.string.unknown_sender)
override fun contactUnknownRecipient(): String = context.getString(R.string.unknown_recipient)
@@ -37,8 +31,6 @@ class K9CoreResourceProvider(private val context: Context) : CoreResourceProvide
override fun replyHeader(sender: String, sentDate: String): String =
context.getString(R.string.message_compose_reply_header_fmt_with_date, sentDate, sender)
- override fun searchAllMessagesTitle(): String = context.getString(R.string.search_all_messages_title)
- override fun searchAllMessagesDetail(): String = context.getString(R.string.search_all_messages_detail)
override fun searchUnifiedInboxTitle(): String = context.getString(R.string.integrated_inbox_title)
override fun searchUnifiedInboxDetail(): String = context.getString(R.string.integrated_inbox_detail)
diff --git a/app/k9mail/src/main/java/com/fsck/k9/widget/list/MessageListRemoteViewFactory.java b/app/k9mail/src/main/java/com/fsck/k9/widget/list/MessageListRemoteViewFactory.java
index f159a43833c510c53c2b36daeb226131f2f6aa78..3d9af2ea24dbe48a3d52cc3ab8a9c98cd78c540d 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/widget/list/MessageListRemoteViewFactory.java
+++ b/app/k9mail/src/main/java/com/fsck/k9/widget/list/MessageListRemoteViewFactory.java
@@ -135,6 +135,7 @@ public class MessageListRemoteViewFactory implements RemoteViewsService.RemoteVi
}
Intent intent = new Intent();
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.setData(item.uri);
remoteView.setOnClickFillInIntent(R.id.mail_list_item, intent);
diff --git a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/KoinModule.kt b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/KoinModule.kt
index 1adad66c0f8538ddd7c13e4ee61f1ca482f92ac1..1cae6a4fde663232db1c709eca35535b489733c2 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/KoinModule.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/KoinModule.kt
@@ -10,11 +10,11 @@ val unreadWidgetModule = module {
preferences = get(),
messagingController = get(),
defaultFolderProvider = get(),
- folderRepositoryManager = get(),
+ folderRepository = get(),
folderNameFormatterFactory = get()
)
}
single { UnreadWidgetUpdater(context = get()) }
single { UnreadWidgetUpdateListener(unreadWidgetUpdater = get()) }
- single { UnreadWidgetMigrations(accountRepository = get(), folderRepositoryManager = get()) }
+ single { UnreadWidgetMigrations(accountRepository = get(), folderRepository = get()) }
}
diff --git a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetConfigurationFragment.kt b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetConfigurationFragment.kt
index 180c8e434858f34cce58903b39c0a8e856f658df..57dc0fc58086cf02123a5b41f81f90416d6b3d17 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetConfigurationFragment.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetConfigurationFragment.kt
@@ -58,6 +58,7 @@ class UnreadWidgetConfigurationFragment : PreferenceFragmentCompat() {
unreadFolder.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = ChooseFolderActivity.buildLaunchIntent(
context = requireContext(),
+ action = ChooseFolderActivity.Action.CHOOSE,
accountUuid = selectedAccountUuid!!,
showDisplayableOnly = true
)
diff --git a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetDataProvider.kt b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetDataProvider.kt
index 1de02427e0702ec6fab5751a4a6dcb66e0de454d..b2b63a82e5f452960e75070045999bb3f1b5b6a0 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetDataProvider.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetDataProvider.kt
@@ -7,7 +7,7 @@ import com.fsck.k9.Preferences
import com.fsck.k9.R
import com.fsck.k9.activity.MessageList
import com.fsck.k9.controller.MessagingController
-import com.fsck.k9.mailstore.FolderRepositoryManager
+import com.fsck.k9.mailstore.FolderRepository
import com.fsck.k9.search.LocalSearch
import com.fsck.k9.search.SearchAccount
import com.fsck.k9.ui.folders.FolderNameFormatterFactory
@@ -19,7 +19,7 @@ class UnreadWidgetDataProvider(
private val preferences: Preferences,
private val messagingController: MessagingController,
private val defaultFolderProvider: DefaultFolderProvider,
- private val folderRepositoryManager: FolderRepositoryManager,
+ private val folderRepository: FolderRepository,
private val folderNameFormatterFactory: FolderNameFormatterFactory
) {
fun loadUnreadWidgetData(configuration: UnreadWidgetConfiguration): UnreadWidgetData? = with(configuration) {
@@ -78,8 +78,7 @@ class UnreadWidgetDataProvider(
}
private fun getFolderDisplayName(account: Account, folderId: Long): String {
- val folderRepository = folderRepositoryManager.getFolderRepository(account)
- val folder = folderRepository.getFolder(folderId)
+ val folder = folderRepository.getFolder(account, folderId)
return if (folder != null) {
val folderNameFormatter = folderNameFormatterFactory.create(context)
folderNameFormatter.displayName(folder)
diff --git a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetMigrations.kt b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetMigrations.kt
index d2b518dcf0f412a94b610d4eb421c4634923e2c6..4221ccef743631a452565beb8c59efe6fddb9b01 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetMigrations.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetMigrations.kt
@@ -3,13 +3,13 @@ package com.fsck.k9.widget.unread
import android.content.SharedPreferences
import androidx.core.content.edit
import com.fsck.k9.Preferences
-import com.fsck.k9.mailstore.FolderRepositoryManager
+import com.fsck.k9.mailstore.FolderRepository
import com.fsck.k9.widget.unread.UnreadWidgetRepository.Companion.PREFS_VERSION
import com.fsck.k9.widget.unread.UnreadWidgetRepository.Companion.PREF_VERSION_KEY
internal class UnreadWidgetMigrations(
private val accountRepository: Preferences,
- private val folderRepositoryManager: FolderRepositoryManager
+ private val folderRepository: FolderRepository
) {
fun upgradePreferences(preferences: SharedPreferences, version: Int) {
if (version < 2) rewriteFolderNameToFolderId(preferences)
@@ -33,8 +33,7 @@ internal class UnreadWidgetMigrations(
val folderServerId = preferences.getString("unread_widget.$widgetId.folder_name", null)
if (folderServerId != null) {
- val folderRepository = folderRepositoryManager.getFolderRepository(account)
- val folderId = folderRepository.getFolderId(folderServerId)
+ val folderId = folderRepository.getFolderId(account, folderServerId)
putString("unread_widget.$widgetId.folder_id", folderId?.toString())
}
diff --git a/app/k9mail/src/main/res/layout/activity_unread_widget_configuration.xml b/app/k9mail/src/main/res/layout/activity_unread_widget_configuration.xml
index 6093c57ecc44fcd59b0a0ff9e0d7acbbbae3036e..aac4c66e5a7e89baf26c9efa58b8986de14f17f9 100644
--- a/app/k9mail/src/main/res/layout/activity_unread_widget_configuration.xml
+++ b/app/k9mail/src/main/res/layout/activity_unread_widget_configuration.xml
@@ -1,8 +1,10 @@
+ android:orientation="vertical"
+ tools:context=".widget.unread.UnreadWidgetConfigurationActivity">
diff --git a/app/k9mail/src/main/res/layout/message_list_widget_list_item.xml b/app/k9mail/src/main/res/layout/message_list_widget_list_item.xml
index b1ab2c66480b8d1683c431b46b3d284a0bd34cf1..368ee21e97af4335a124510b6d07fd2338e60520 100644
--- a/app/k9mail/src/main/res/layout/message_list_widget_list_item.xml
+++ b/app/k9mail/src/main/res/layout/message_list_widget_list_item.xml
@@ -24,9 +24,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
- android:layout_alignParentRight="true"
android:layout_marginStart="4dp"
- android:layout_marginLeft="4dp"
tools:text="25 May" />
@@ -48,11 +44,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
- android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_gravity="start"
android:layout_toStartOf="@id/attachment"
- android:layout_toLeftOf="@id/attachment"
android:ellipsize="end"
android:maxLines="1"
android:textSize="16sp"
@@ -64,7 +58,6 @@
android:layout_height="wrap_content"
android:layout_below="@+id/sender"
android:layout_alignParentStart="true"
- android:layout_alignParentLeft="true"
android:ellipsize="end"
android:maxLines="1"
android:paddingBottom="2dp"
@@ -77,7 +70,6 @@
android:layout_height="wrap_content"
android:layout_below="@+id/mail_subject"
android:layout_alignParentStart="true"
- android:layout_alignParentLeft="true"
android:maxLines="1"
android:textSize="13sp"
tools:text="Towel Day is celebrated every year on 25 May as a tribute to the author Douglas Adams by his fans." />
diff --git a/app/k9mail/src/main/res/xml/network_security_config.xml b/app/k9mail/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000000000000000000000000000000000000..094d43225127492bdcb2444b0c828a628d332cb8
--- /dev/null
+++ b/app/k9mail/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,12 @@
+
+
+
+
+