diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 6dda57b114e435c30ebd75d078567ea0c1b6b6ac..0e85d8939692dcc48ad740f28a29f85daa62c407 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -13,4 +13,4 @@ jobs: with: java-version: 11 - name: Build with Gradle - run: ./gradlew assembleDebug testDebugUnitTest ktlintCheck + run: ./gradlew assembleDebug ktlintCheck testsOnCi diff --git a/app/autodiscovery/providersxml/build.gradle b/app/autodiscovery/providersxml/build.gradle index 18cc6a8682470f1ad52cf33ace9487dc07c7220c..9cd4bae54b3a23ce6a27aa15b683892fd135ecc2 100644 --- a/app/autodiscovery/providersxml/build.gradle +++ b/app/autodiscovery/providersxml/build.gradle @@ -14,7 +14,7 @@ dependencies { testImplementation "androidx.test:core:${versions.androidxTestCore}" testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" + testImplementation "org.mockito:mockito-inline:${versions.mockito}" testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}" } diff --git a/app/autodiscovery/providersxml/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/autodiscovery/providersxml/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d450f0dc49ca715b1a0a88a5aa746ee11e..0000000000000000000000000000000000000000 --- a/app/autodiscovery/providersxml/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline diff --git a/app/autodiscovery/srvrecords/build.gradle b/app/autodiscovery/srvrecords/build.gradle index 2ebb818f17020b4170cc45deb8b2fd62b157fa3f..061b93041d30f6ec8468bb75d5455abd99ab523c 100644 --- a/app/autodiscovery/srvrecords/build.gradle +++ b/app/autodiscovery/srvrecords/build.gradle @@ -15,7 +15,7 @@ dependencies { testImplementation "androidx.test:core:${versions.androidxTestCore}" testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" + testImplementation "org.mockito:mockito-inline:${versions.mockito}" testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}" } diff --git a/app/autodiscovery/srvrecords/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/autodiscovery/srvrecords/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d450f0dc49ca715b1a0a88a5aa746ee11e..0000000000000000000000000000000000000000 --- a/app/autodiscovery/srvrecords/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline diff --git a/app/autodiscovery/thunderbird/build.gradle b/app/autodiscovery/thunderbird/build.gradle index ed21e968f289f614f7682c7db5425e7b64c47a0e..11007e72060ba3d86c8240547c03f612f0d33f3d 100644 --- a/app/autodiscovery/thunderbird/build.gradle +++ b/app/autodiscovery/thunderbird/build.gradle @@ -15,7 +15,7 @@ dependencies { testImplementation "androidx.test:core:${versions.androidxTestCore}" testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" + testImplementation "org.mockito:mockito-inline:${versions.mockito}" testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}" } diff --git a/app/autodiscovery/thunderbird/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/autodiscovery/thunderbird/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d450f0dc49ca715b1a0a88a5aa746ee11e..0000000000000000000000000000000000000000 --- a/app/autodiscovery/thunderbird/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline diff --git a/app/core/build.gradle b/app/core/build.gradle index 8bb6de20722cd8dfd153ac6b4decdf149e85effd..c33b91510ef10003b3f50e787820f853a531d3fc 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'org.jetbrains.kotlin.plugin.parcelize' dependencies { api project(":mail:common") api project(":backend:api") + api project(":app:html-cleaner") implementation project(':plugins:openpgp-api-lib:openpgp-api') @@ -18,7 +19,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:${versions.androidxWorkManager}" implementation "androidx.fragment:fragment:${versions.androidxFragment}" implementation "androidx.localbroadcastmanager:localbroadcastmanager:${versions.androidxLocalBroadcastManager}" - implementation "org.jsoup:jsoup:1.13.1" + implementation "org.jsoup:jsoup:${versions.jsoup}" implementation "com.squareup.moshi:moshi:${versions.moshi}" implementation "com.jakewharton.timber:timber:${versions.timber}" implementation "org.apache.james:apache-mime4j-core:${versions.mime4j}" @@ -32,7 +33,7 @@ dependencies { testImplementation "org.robolectric:robolectric:${versions.robolectric}" testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" + testImplementation "org.mockito:mockito-inline:${versions.mockito}" testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" testImplementation "org.jdom:jdom2:2.0.6" testImplementation "io.insert-koin:koin-test:${versions.koin}" diff --git a/app/core/src/main/java/com/fsck/k9/Account.java b/app/core/src/main/java/com/fsck/k9/Account.java deleted file mode 100644 index 31cc552e2292c1fe8a26563159d7b2d8b9db98c2..0000000000000000000000000000000000000000 --- a/app/core/src/main/java/com/fsck/k9/Account.java +++ /dev/null @@ -1,1107 +0,0 @@ - -package com.fsck.k9; - - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -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 org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - - -/** - * Account stores all of the settings for a single account defined by the user. Each account is defined by a UUID. - */ -public class Account implements BaseAccount { - /** - * Fixed name of outbox - not actually displayed. - */ - public static final String OUTBOX_NAME = "Outbox"; - - public enum Expunge { - EXPUNGE_IMMEDIATELY, - EXPUNGE_MANUALLY, - EXPUNGE_ON_POLL; - - public ExpungePolicy toBackendExpungePolicy() { - switch (this) { - case EXPUNGE_IMMEDIATELY: return ExpungePolicy.IMMEDIATELY; - case EXPUNGE_MANUALLY: return ExpungePolicy.MANUALLY; - case EXPUNGE_ON_POLL: return ExpungePolicy.ON_POLL; - } - - throw new AssertionError("Unhandled case: " + this.name()); - } - } - - public enum DeletePolicy { - NEVER(0), - SEVEN_DAYS(1), - ON_DELETE(2), - MARK_AS_READ(3); - - public final int setting; - - DeletePolicy(int setting) { - this.setting = setting; - } - - public static DeletePolicy fromInt(int initialSetting) { - for (DeletePolicy policy: values()) { - if (policy.setting == initialSetting) { - return policy; - } - } - throw new IllegalArgumentException("DeletePolicy " + initialSetting + " unknown"); - } - } - - public enum SortType { - SORT_DATE(false), - SORT_ARRIVAL(false), - SORT_SUBJECT(true), - SORT_SENDER(true), - SORT_UNREAD(true), - SORT_FLAGGED(true), - SORT_ATTACHMENT(true); - - private boolean defaultAscending; - - SortType(boolean defaultAscending) { - this.defaultAscending = defaultAscending; - } - - public boolean isDefaultAscending() { - return defaultAscending; - } - } - - public static final SortType DEFAULT_SORT_TYPE = SortType.SORT_DATE; - public static final boolean DEFAULT_SORT_ASCENDING = false; - public static final long NO_OPENPGP_KEY = 0; - public static final int UNASSIGNED_ACCOUNT_NUMBER = -1; - - public static final int INTERVAL_MINUTES_NEVER = -1; - public static final int DEFAULT_SYNC_INTERVAL = 5; - - private DeletePolicy deletePolicy = DeletePolicy.NEVER; - - private final String accountUuid; - private ServerSettings incomingServerSettings; - private ServerSettings outgoingServerSettings; - - /** - * Storage provider ID, used to locate and manage the underlying DB/file - * storage - */ - private String localStorageProviderId; - private String description; - private String alwaysBcc; - private int automaticCheckIntervalMinutes; - private int displayCount; - private int chipColor; - private boolean notifyNewMail; - private FolderMode folderNotifyNewMailMode; - private boolean notifySelfNewMail; - private boolean notifyContactsMailOnly; - private boolean ignoreChatMessages; - private String legacyInboxFolder; - private String importedDraftsFolder; - private String importedSentFolder; - private String importedTrashFolder; - private String importedArchiveFolder; - private String importedSpamFolder; - private Long inboxFolderId; - private Long outboxFolderId; - private Long draftsFolderId; - private Long sentFolderId; - private Long trashFolderId; - private Long archiveFolderId; - private Long spamFolderId; - private SpecialFolderSelection draftsFolderSelection; - private SpecialFolderSelection sentFolderSelection; - private SpecialFolderSelection trashFolderSelection; - private SpecialFolderSelection archiveFolderSelection; - private SpecialFolderSelection spamFolderSelection; - private String importedAutoExpandFolder; - private Long autoExpandFolderId; - private FolderMode folderDisplayMode; - private FolderMode folderSyncMode; - private FolderMode folderPushMode; - private FolderMode folderTargetMode; - private int accountNumber; - private boolean notifySync; - private SortType sortType; - private Map sortAscending = new HashMap<>(); - private ShowPictures showPictures; - private boolean isSignatureBeforeQuotedText; - private Expunge expungePolicy = Expunge.EXPUNGE_IMMEDIATELY; - private int maxPushFolders; - private int idleRefreshMinutes; - private final Map compressionMap = new ConcurrentHashMap<>(); - private Searchable searchableFolders; - private boolean subscribedFoldersOnly; - private int maximumPolledMessageAge; - private int maximumAutoDownloadMessageSize; - private MessageFormat messageFormat; - private boolean messageFormatAuto; - private boolean messageReadReceipt; - private QuoteStyle quoteStyle; - private String quotePrefix; - private boolean defaultQuotedTextShown; - private boolean replyAfterQuote; - private boolean stripSignature; - private boolean syncRemoteDeletions; - private String openPgpProvider; - private long openPgpKey; - private boolean autocryptPreferEncryptMutual; - private boolean openPgpHideSignOnly; - private boolean openPgpEncryptSubject; - private boolean openPgpEncryptAllDrafts; - private boolean markMessageAsReadOnView; - private boolean markMessageAsReadOnDelete; - private boolean alwaysShowCcBcc; - private boolean allowRemoteSearch; - private boolean remoteSearchFullText; - private int remoteSearchNumResults; - private boolean uploadSentMessages; - private long lastSyncTime; - private long lastFolderListRefreshTime; - private boolean isFinishedSetup = false; - private int messagesNotificationChannelVersion; - - private boolean changedVisibleLimits = false; - - /** - * Database ID of the folder that was last selected for a copy or move operation. - * - * Note: For now this value isn't persisted. So it will be reset when K-9 Mail is restarted. - */ - private Long lastSelectedFolderId = null; - - private List identities; - - private final NotificationSetting notificationSetting = new NotificationSetting(); - - public enum FolderMode { - NONE, ALL, FIRST_CLASS, FIRST_AND_SECOND_CLASS, NOT_SECOND_CLASS - } - - public enum SpecialFolderSelection { - AUTOMATIC, - MANUAL - } - - public enum ShowPictures { - NEVER, ALWAYS, ONLY_FROM_CONTACTS - } - - public enum Searchable { - ALL, DISPLAYABLE, NONE - } - - public enum QuoteStyle { - PREFIX, HEADER - } - - public enum MessageFormat { - TEXT, HTML, AUTO - } - - - public Account(String uuid) { - this.accountUuid = uuid; - } - - public synchronized void setChipColor(int color) { - chipColor = color; - } - - public synchronized int getChipColor() { - return chipColor; - } - - @Override - public String getUuid() { - return accountUuid; - } - - public synchronized ServerSettings getIncomingServerSettings() { - return incomingServerSettings; - } - - public synchronized void setIncomingServerSettings(ServerSettings incomingServerSettings) { - this.incomingServerSettings = incomingServerSettings; - } - - public synchronized ServerSettings getOutgoingServerSettings() { - return outgoingServerSettings; - } - - public synchronized void setOutgoingServerSettings(ServerSettings outgoingServerSettings) { - this.outgoingServerSettings = outgoingServerSettings; - } - - @Override - public synchronized String getDescription() { - return description; - } - - public synchronized void setDescription(String description) { - this.description = description; - } - - public String getDisplayName() { - return description != null ? description : getEmail(); - } - - public synchronized String getName() { - return identities.get(0).getName(); - } - - public synchronized void setName(String name) { - Identity newIdentity = identities.get(0).withName(name); - identities.set(0, newIdentity); - } - - public synchronized boolean getSignatureUse() { - return identities.get(0).getSignatureUse(); - } - - public synchronized void setSignatureUse(boolean signatureUse) { - Identity newIdentity = identities.get(0).withSignatureUse(signatureUse); - identities.set(0, newIdentity); - } - - public synchronized String getSignature() { - return identities.get(0).getSignature(); - } - - public synchronized void setSignature(String signature) { - Identity newIdentity = identities.get(0).withSignature(signature); - identities.set(0, newIdentity); - } - - @Override - public synchronized String getEmail() { - return identities.get(0).getEmail(); - } - - public synchronized void setEmail(String email) { - Identity newIdentity = identities.get(0).withEmail(email); - identities.set(0, newIdentity); - } - - public synchronized String getAlwaysBcc() { - return alwaysBcc; - } - - public synchronized void setAlwaysBcc(String alwaysBcc) { - this.alwaysBcc = alwaysBcc; - } - - public String getLocalStorageProviderId() { - return localStorageProviderId; - } - - public void setLocalStorageProviderId(String id) { - localStorageProviderId = id; - } - - /** - * Returns -1 for never. - */ - public synchronized int getAutomaticCheckIntervalMinutes() { - return automaticCheckIntervalMinutes; - } - - /** - * @param automaticCheckIntervalMinutes or -1 for never. - */ - public synchronized boolean setAutomaticCheckIntervalMinutes(int automaticCheckIntervalMinutes) { - int oldInterval = this.automaticCheckIntervalMinutes; - this.automaticCheckIntervalMinutes = automaticCheckIntervalMinutes; - - return (oldInterval != automaticCheckIntervalMinutes); - } - - public synchronized int getDisplayCount() { - return displayCount; - } - - public synchronized void setDisplayCount(int displayCount) { - if (this.displayCount == displayCount) { - return; - } - - if (displayCount != -1) { - this.displayCount = displayCount; - } else { - this.displayCount = K9.DEFAULT_VISIBLE_LIMIT; - } - - changedVisibleLimits = true; - } - - public synchronized boolean isNotifyNewMail() { - return notifyNewMail; - } - - public synchronized void setNotifyNewMail(boolean notifyNewMail) { - this.notifyNewMail = notifyNewMail; - } - - public synchronized FolderMode getFolderNotifyNewMailMode() { - return folderNotifyNewMailMode; - } - - public synchronized void setFolderNotifyNewMailMode(FolderMode folderNotifyNewMailMode) { - this.folderNotifyNewMailMode = folderNotifyNewMailMode; - } - - public synchronized DeletePolicy getDeletePolicy() { - return deletePolicy; - } - - public synchronized void setDeletePolicy(DeletePolicy deletePolicy) { - this.deletePolicy = deletePolicy; - } - - public synchronized String getImportedDraftsFolder() { - return importedDraftsFolder; - } - - public synchronized void setImportedDraftsFolder(String folderServerId) { - importedDraftsFolder = folderServerId; - } - - @Nullable - public synchronized Long getDraftsFolderId() { - return draftsFolderId; - } - - public synchronized void setDraftsFolderId(@Nullable Long folderId) { - draftsFolderId = folderId; - } - - public synchronized void setDraftsFolderId(@Nullable Long folderId, SpecialFolderSelection selection) { - draftsFolderId = folderId; - draftsFolderSelection = selection; - } - - public synchronized boolean hasDraftsFolder() { - return draftsFolderId != null; - } - - public synchronized String getImportedSentFolder() { - return importedSentFolder; - } - - public synchronized void setImportedSentFolder(String folderServerId) { - importedSentFolder = folderServerId; - } - - @Nullable - public synchronized Long getSentFolderId() { - return sentFolderId; - } - - public synchronized void setSentFolderId(@Nullable Long folderId) { - sentFolderId = folderId; - } - - public synchronized void setSentFolderId(@Nullable Long folderId, SpecialFolderSelection selection) { - sentFolderId = folderId; - sentFolderSelection = selection; - } - - public synchronized boolean hasSentFolder() { - return sentFolderId != null; - } - - public synchronized String getImportedTrashFolder() { - return importedTrashFolder; - } - - public synchronized void setImportedTrashFolder(String folderServerId) { - importedTrashFolder = folderServerId; - } - - @Nullable - public synchronized Long getTrashFolderId() { - return trashFolderId; - } - - public synchronized void setTrashFolderId(@Nullable Long folderId) { - trashFolderId = folderId; - } - - public synchronized void setTrashFolderId(@Nullable Long folderId, SpecialFolderSelection selection) { - trashFolderId = folderId; - trashFolderSelection = selection; - } - - public synchronized boolean hasTrashFolder() { - return trashFolderId != null; - } - - public synchronized String getImportedArchiveFolder() { - return importedArchiveFolder; - } - - public synchronized void setImportedArchiveFolder(String archiveFolder) { - this.importedArchiveFolder = archiveFolder; - } - - @Nullable - public synchronized Long getArchiveFolderId() { - return archiveFolderId; - } - - public synchronized void setArchiveFolderId(@Nullable Long folderId) { - archiveFolderId = folderId; - } - - public synchronized void setArchiveFolderId(@Nullable Long folderId, SpecialFolderSelection selection) { - this.archiveFolderId = folderId; - archiveFolderSelection = selection; - } - - public synchronized boolean hasArchiveFolder() { - return archiveFolderId != null; - } - - public synchronized String getImportedSpamFolder() { - return importedSpamFolder; - } - - public synchronized void setImportedSpamFolder(String folderServerId) { - importedSpamFolder = folderServerId; - } - - @Nullable - public synchronized Long getSpamFolderId() { - return spamFolderId; - } - - public synchronized void setSpamFolderId(@Nullable Long folderId) { - spamFolderId = folderId; - } - - public synchronized void setSpamFolderId(@Nullable Long folderId, SpecialFolderSelection selection) { - spamFolderId = folderId; - spamFolderSelection = selection; - } - - public synchronized boolean hasSpamFolder() { - return spamFolderId != null; - } - - @NotNull - public SpecialFolderSelection getDraftsFolderSelection() { - return draftsFolderSelection; - } - - @NotNull - public synchronized SpecialFolderSelection getSentFolderSelection() { - return sentFolderSelection; - } - - @NotNull - public synchronized SpecialFolderSelection getTrashFolderSelection() { - return trashFolderSelection; - } - - @NotNull - public synchronized SpecialFolderSelection getArchiveFolderSelection() { - return archiveFolderSelection; - } - - @NotNull - public synchronized SpecialFolderSelection getSpamFolderSelection() { - return spamFolderSelection; - } - - @Nullable - public synchronized Long getOutboxFolderId() { - return outboxFolderId; - } - - public synchronized void setOutboxFolderId(@Nullable Long folderId) { - outboxFolderId = folderId; - } - - public synchronized String getImportedAutoExpandFolder() { - return importedAutoExpandFolder; - } - - public synchronized void setImportedAutoExpandFolder(String name) { - importedAutoExpandFolder = name; - } - - @Nullable - public synchronized Long getAutoExpandFolderId() { - return autoExpandFolderId; - } - - public synchronized void setAutoExpandFolderId(@Nullable Long folderId) { - autoExpandFolderId = folderId; - } - - public synchronized int getAccountNumber() { - return accountNumber; - } - - public synchronized void setAccountNumber(int accountNumber) { - this.accountNumber = accountNumber; - } - - public synchronized FolderMode getFolderDisplayMode() { - return folderDisplayMode; - } - - public synchronized boolean setFolderDisplayMode(FolderMode displayMode) { - FolderMode oldDisplayMode = folderDisplayMode; - folderDisplayMode = displayMode; - return oldDisplayMode != displayMode; - } - - public synchronized FolderMode getFolderSyncMode() { - return folderSyncMode; - } - - public synchronized boolean setFolderSyncMode(FolderMode syncMode) { - FolderMode oldSyncMode = folderSyncMode; - folderSyncMode = syncMode; - - if (syncMode == FolderMode.NONE && oldSyncMode != FolderMode.NONE) { - return true; - } - return syncMode != FolderMode.NONE && oldSyncMode == FolderMode.NONE; - } - - public synchronized FolderMode getFolderPushMode() { - return folderPushMode; - } - - public synchronized void setFolderPushMode(FolderMode pushMode) { - folderPushMode = pushMode; - } - - public synchronized boolean isNotifySync() { - return notifySync; - } - - public synchronized void setNotifySync(boolean notifySync) { - 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; - } - - public synchronized void setSortType(SortType sortType) { - this.sortType = sortType; - } - - public synchronized boolean isSortAscending(SortType sortType) { - if (sortAscending.get(sortType) == null) { - sortAscending.put(sortType, sortType.isDefaultAscending()); - } - return sortAscending.get(sortType); - } - - public synchronized void setSortAscending(SortType sortType, boolean sortAscending) { - this.sortAscending.put(sortType, sortAscending); - } - - public synchronized ShowPictures getShowPictures() { - return showPictures; - } - - public synchronized void setShowPictures(ShowPictures showPictures) { - this.showPictures = showPictures; - } - - public synchronized FolderMode getFolderTargetMode() { - return folderTargetMode; - } - - public synchronized void setFolderTargetMode(FolderMode folderTargetMode) { - this.folderTargetMode = folderTargetMode; - } - - public synchronized boolean isSignatureBeforeQuotedText() { - return isSignatureBeforeQuotedText; - } - - public synchronized void setSignatureBeforeQuotedText(boolean mIsSignatureBeforeQuotedText) { - this.isSignatureBeforeQuotedText = mIsSignatureBeforeQuotedText; - } - - public synchronized boolean isNotifySelfNewMail() { - return notifySelfNewMail; - } - - public synchronized void setNotifySelfNewMail(boolean notifySelfNewMail) { - this.notifySelfNewMail = notifySelfNewMail; - } - - public synchronized boolean isNotifyContactsMailOnly() { - return notifyContactsMailOnly; - } - - public synchronized void setNotifyContactsMailOnly(boolean notifyContactsMailOnly) { - this.notifyContactsMailOnly = notifyContactsMailOnly; - } - - public synchronized boolean isIgnoreChatMessages() { - return ignoreChatMessages; - } - - public synchronized void setIgnoreChatMessages(boolean ignoreChatMessages) { - this.ignoreChatMessages = ignoreChatMessages; - } - - public synchronized Expunge getExpungePolicy() { - return expungePolicy; - } - - public synchronized void setExpungePolicy(Expunge expungePolicy) { - this.expungePolicy = expungePolicy; - } - - public synchronized int getMaxPushFolders() { - return maxPushFolders; - } - - public synchronized boolean setMaxPushFolders(int maxPushFolders) { - int oldMaxPushFolders = this.maxPushFolders; - this.maxPushFolders = maxPushFolders; - return oldMaxPushFolders != maxPushFolders; - } - - @Override - public synchronized String toString() { - return description; - } - - public synchronized void setCompression(NetworkType networkType, boolean useCompression) { - compressionMap.put(networkType, useCompression); - } - - public synchronized boolean useCompression(NetworkType networkType) { - Boolean useCompression = compressionMap.get(networkType); - if (useCompression == null) { - return true; - } - - return useCompression; - } - - public Map getCompressionMap() { - return Collections.unmodifiableMap(compressionMap); - } - - @Override - public boolean equals(Object o) { - if (o instanceof Account) { - return ((Account)o).accountUuid.equals(accountUuid); - } - return super.equals(o); - } - - @Override - public int hashCode() { - return accountUuid.hashCode(); - } - - public synchronized List getIdentities() { - return identities; - } - - public synchronized void setIdentities(List newIdentities) { - identities = new ArrayList<>(newIdentities); - } - - public synchronized Identity getIdentity(int i) { - if (i < identities.size()) { - return identities.get(i); - } - throw new IllegalArgumentException("Identity with index " + i + " not found"); - } - - public boolean isAnIdentity(Address[] addrs) { - if (addrs == null) { - return false; - } - for (Address addr : addrs) { - if (findIdentity(addr) != null) { - return true; - } - } - - return false; - } - - public boolean isAnIdentity(Address addr) { - return findIdentity(addr) != null; - } - - public synchronized Identity findIdentity(Address addr) { - for (Identity identity : identities) { - String email = identity.getEmail(); - if (email != null && email.equalsIgnoreCase(addr.getAddress())) { - return identity; - } - } - return null; - } - - public synchronized Searchable getSearchableFolders() { - return searchableFolders; - } - - public synchronized void setSearchableFolders(Searchable searchableFolders) { - this.searchableFolders = searchableFolders; - } - - public synchronized int getIdleRefreshMinutes() { - return idleRefreshMinutes; - } - - public synchronized void setIdleRefreshMinutes(int idleRefreshMinutes) { - this.idleRefreshMinutes = idleRefreshMinutes; - } - - public synchronized boolean isSubscribedFoldersOnly() { - return subscribedFoldersOnly; - } - - public synchronized void setSubscribedFoldersOnly(boolean subscribedFoldersOnly) { - this.subscribedFoldersOnly = subscribedFoldersOnly; - } - - public synchronized int getMaximumPolledMessageAge() { - return maximumPolledMessageAge; - } - - public synchronized void setMaximumPolledMessageAge(int maximumPolledMessageAge) { - this.maximumPolledMessageAge = maximumPolledMessageAge; - } - - public synchronized int getMaximumAutoDownloadMessageSize() { - return maximumAutoDownloadMessageSize; - } - - public synchronized void setMaximumAutoDownloadMessageSize(int maximumAutoDownloadMessageSize) { - this.maximumAutoDownloadMessageSize = maximumAutoDownloadMessageSize; - } - - public Date getEarliestPollDate() { - int age = getMaximumPolledMessageAge(); - if (age >= 0) { - Calendar now = Calendar.getInstance(); - now.set(Calendar.HOUR_OF_DAY, 0); - now.set(Calendar.MINUTE, 0); - now.set(Calendar.SECOND, 0); - now.set(Calendar.MILLISECOND, 0); - if (age < 28) { - now.add(Calendar.DATE, age * -1); - } else switch (age) { - case 28: - now.add(Calendar.MONTH, -1); - break; - case 56: - now.add(Calendar.MONTH, -2); - break; - case 84: - now.add(Calendar.MONTH, -3); - break; - case 168: - now.add(Calendar.MONTH, -6); - break; - case 365: - now.add(Calendar.YEAR, -1); - break; - } - - return now.getTime(); - } - - return null; - } - - public MessageFormat getMessageFormat() { - return messageFormat; - } - - public void setMessageFormat(MessageFormat messageFormat) { - this.messageFormat = messageFormat; - } - - public synchronized boolean isMessageFormatAuto() { - return messageFormatAuto; - } - - public synchronized void setMessageFormatAuto(boolean messageFormatAuto) { - this.messageFormatAuto = messageFormatAuto; - } - - public synchronized boolean isMessageReadReceipt() { - return messageReadReceipt; - } - - public synchronized void setMessageReadReceipt(boolean messageReadReceipt) { - this.messageReadReceipt = messageReadReceipt; - } - - public QuoteStyle getQuoteStyle() { - return quoteStyle; - } - - public void setQuoteStyle(QuoteStyle quoteStyle) { - this.quoteStyle = quoteStyle; - } - - public synchronized String getQuotePrefix() { - return quotePrefix; - } - - public synchronized void setQuotePrefix(String quotePrefix) { - this.quotePrefix = quotePrefix; - } - - public synchronized boolean isDefaultQuotedTextShown() { - return defaultQuotedTextShown; - } - - public synchronized void setDefaultQuotedTextShown(boolean shown) { - defaultQuotedTextShown = shown; - } - - public synchronized boolean isReplyAfterQuote() { - return replyAfterQuote; - } - - public synchronized void setReplyAfterQuote(boolean replyAfterQuote) { - this.replyAfterQuote = replyAfterQuote; - } - - public synchronized boolean isStripSignature() { - return stripSignature; - } - - public synchronized void setStripSignature(boolean stripSignature) { - this.stripSignature = stripSignature; - } - - public boolean isOpenPgpProviderConfigured() { - return !TextUtils.isEmpty(openPgpProvider); - } - - @Nullable - public String getOpenPgpProvider() { - if (TextUtils.isEmpty(openPgpProvider)) { - return null; - } - return openPgpProvider; - } - - public void setOpenPgpProvider(String openPgpProvider) { - this.openPgpProvider = openPgpProvider; - } - - public long getOpenPgpKey() { - return openPgpKey; - } - - public void setOpenPgpKey(long keyId) { - openPgpKey = keyId; - } - - public boolean hasOpenPgpKey() { - return openPgpKey != NO_OPENPGP_KEY; - } - - public boolean getAutocryptPreferEncryptMutual() { - return autocryptPreferEncryptMutual; - } - - public void setAutocryptPreferEncryptMutual(boolean autocryptPreferEncryptMutual) { - this.autocryptPreferEncryptMutual = autocryptPreferEncryptMutual; - } - - public boolean isOpenPgpHideSignOnly() { - return openPgpHideSignOnly; - } - - public void setOpenPgpHideSignOnly(boolean openPgpHideSignOnly) { - this.openPgpHideSignOnly = openPgpHideSignOnly; - } - - public boolean isOpenPgpEncryptSubject() { - return openPgpEncryptSubject; - } - - public void setOpenPgpEncryptSubject(boolean openPgpEncryptSubject) { - this.openPgpEncryptSubject = openPgpEncryptSubject; - } - - public boolean isOpenPgpEncryptAllDrafts() { - return openPgpEncryptAllDrafts; - } - - public void setOpenPgpEncryptAllDrafts(boolean openPgpEncryptAllDrafts) { - this.openPgpEncryptAllDrafts = openPgpEncryptAllDrafts; - } - - public boolean isAllowRemoteSearch() { - return allowRemoteSearch; - } - - public void setAllowRemoteSearch(boolean val) { - allowRemoteSearch = val; - } - - public int getRemoteSearchNumResults() { - return remoteSearchNumResults; - } - - public void setRemoteSearchNumResults(int val) { - remoteSearchNumResults = (val >= 0 ? val : 0); - } - - public boolean isUploadSentMessages() { - return uploadSentMessages; - } - - public void setUploadSentMessages(boolean uploadSentMessages) { - this.uploadSentMessages = uploadSentMessages; - } - - public String getLegacyInboxFolder() { - return legacyInboxFolder; - } - - void setLegacyInboxFolder(String name) { - this.legacyInboxFolder = name; - } - - @Nullable - public synchronized Long getInboxFolderId() { - return inboxFolderId; - } - - public synchronized void setInboxFolderId(@Nullable Long folderId) { - inboxFolderId = folderId; - } - - public synchronized boolean isSyncRemoteDeletions() { - return syncRemoteDeletions; - } - - public synchronized void setSyncRemoteDeletions(boolean syncRemoteDeletions) { - this.syncRemoteDeletions = syncRemoteDeletions; - } - - public synchronized Long getLastSelectedFolderId() { - return lastSelectedFolderId; - } - - public synchronized void setLastSelectedFolderId(long folderId) { - lastSelectedFolderId = folderId; - } - - public synchronized NotificationSetting getNotificationSetting() { - return notificationSetting; - } - - public synchronized boolean isMarkMessageAsReadOnView() { - return markMessageAsReadOnView; - } - - public synchronized void setMarkMessageAsReadOnView(boolean value) { - markMessageAsReadOnView = value; - } - - public synchronized boolean isMarkMessageAsReadOnDelete() { - return markMessageAsReadOnDelete; - } - - public synchronized void setMarkMessageAsReadOnDelete(boolean value) { - markMessageAsReadOnDelete = value; - } - - public synchronized boolean isAlwaysShowCcBcc() { - return alwaysShowCcBcc; - } - - public synchronized void setAlwaysShowCcBcc(boolean show) { - alwaysShowCcBcc = show; - } - public boolean isRemoteSearchFullText() { - return false; // Temporarily disabled - //return remoteSearchFullText; - } - - public void setRemoteSearchFullText(boolean val) { - remoteSearchFullText = val; - } - - public synchronized long getLastSyncTime() { - return lastSyncTime; - } - - public synchronized void setLastSyncTime(long lastSyncTime) { - this.lastSyncTime = lastSyncTime; - } - - public synchronized long getLastFolderListRefreshTime() { - return lastFolderListRefreshTime; - } - - public synchronized void setLastFolderListRefreshTime(long lastFolderListRefreshTime) { - this.lastFolderListRefreshTime = lastFolderListRefreshTime; - } - - boolean isChangedVisibleLimits() { - return changedVisibleLimits; - } - - void resetChangeMarkers() { - changedVisibleLimits = false; - } - - public synchronized boolean isFinishedSetup() { - return isFinishedSetup; - } - - public void markSetupFinished() { - isFinishedSetup = true; - } -} diff --git a/app/core/src/main/java/com/fsck/k9/Account.kt b/app/core/src/main/java/com/fsck/k9/Account.kt new file mode 100644 index 0000000000000000000000000000000000000000..b32e628e704dbc9556c63c663db59a001eb5b236 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/Account.kt @@ -0,0 +1,703 @@ +package com.fsck.k9 + +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 java.util.Calendar +import java.util.Date +import java.util.concurrent.ConcurrentHashMap + +/** + * Account stores all of the settings for a single account defined by the user. Each account is defined by a UUID. + */ +class Account(override val uuid: String) : BaseAccount { + @get:Synchronized + @set:Synchronized + var deletePolicy = DeletePolicy.NEVER + + @get:Synchronized + @set:Synchronized + private var internalIncomingServerSettings: ServerSettings? = null + + @get:Synchronized + @set:Synchronized + private var internalOutgoingServerSettings: ServerSettings? = null + + var incomingServerSettings: ServerSettings + get() = internalIncomingServerSettings ?: error("Incoming server settings not set yet") + set(value) { + internalIncomingServerSettings = value + } + + var outgoingServerSettings: ServerSettings + get() = internalOutgoingServerSettings ?: error("Outgoing server settings not set yet") + set(value) { + internalOutgoingServerSettings = value + } + + /** + * Storage provider ID, used to locate and manage the underlying DB/file storage. + */ + @get:Synchronized + @set:Synchronized + var localStorageProviderId: String? = null + + @get:Synchronized + @set:Synchronized + override var name: String? = null + set(value) { + field = value?.takeIf { it.isNotEmpty() } + } + + @get:Synchronized + @set:Synchronized + var alwaysBcc: String? = null + + /** + * -1 for never. + */ + @get:Synchronized + @set:Synchronized + var automaticCheckIntervalMinutes = 0 + + @get:Synchronized + @set:Synchronized + var displayCount = 0 + set(value) { + if (field != value) { + field = value.takeIf { it != -1 } ?: K9.DEFAULT_VISIBLE_LIMIT + isChangedVisibleLimits = true + } + } + + @get:Synchronized + @set:Synchronized + var chipColor = 0 + + @get:Synchronized + @set:Synchronized + var isNotifyNewMail = false + + @get:Synchronized + @set:Synchronized + var folderNotifyNewMailMode = FolderMode.ALL + + @get:Synchronized + @set:Synchronized + var isNotifySelfNewMail = false + + @get:Synchronized + @set:Synchronized + var isNotifyContactsMailOnly = false + + @get:Synchronized + @set:Synchronized + var isIgnoreChatMessages = false + + @get:Synchronized + @set:Synchronized + var legacyInboxFolder: String? = null + + @get:Synchronized + @set:Synchronized + var importedDraftsFolder: String? = null + + @get:Synchronized + @set:Synchronized + var importedSentFolder: String? = null + + @get:Synchronized + @set:Synchronized + var importedTrashFolder: String? = null + + @get:Synchronized + @set:Synchronized + var importedArchiveFolder: String? = null + + @get:Synchronized + @set:Synchronized + var importedSpamFolder: String? = null + + @get:Synchronized + @set:Synchronized + var inboxFolderId: Long? = null + + @get:Synchronized + @set:Synchronized + var outboxFolderId: Long? = null + + @get:Synchronized + @set:Synchronized + var draftsFolderId: Long? = null + + @get:Synchronized + @set:Synchronized + var sentFolderId: Long? = null + + @get:Synchronized + @set:Synchronized + var trashFolderId: Long? = null + + @get:Synchronized + @set:Synchronized + var archiveFolderId: Long? = null + + @get:Synchronized + @set:Synchronized + var spamFolderId: Long? = null + + @get:Synchronized + var draftsFolderSelection = SpecialFolderSelection.AUTOMATIC + private set + + @get:Synchronized + var sentFolderSelection = SpecialFolderSelection.AUTOMATIC + private set + + @get:Synchronized + var trashFolderSelection = SpecialFolderSelection.AUTOMATIC + private set + + @get:Synchronized + var archiveFolderSelection = SpecialFolderSelection.AUTOMATIC + private set + + @get:Synchronized + var spamFolderSelection = SpecialFolderSelection.AUTOMATIC + private set + + @get:Synchronized + @set:Synchronized + var importedAutoExpandFolder: String? = null + + @get:Synchronized + @set:Synchronized + var autoExpandFolderId: Long? = null + + @get:Synchronized + @set:Synchronized + var folderDisplayMode = FolderMode.NOT_SECOND_CLASS + + @get:Synchronized + @set:Synchronized + var folderSyncMode = FolderMode.FIRST_CLASS + + @get:Synchronized + @set:Synchronized + var folderPushMode = FolderMode.NONE + + @get:Synchronized + @set:Synchronized + var folderTargetMode = FolderMode.NOT_SECOND_CLASS + + @get:Synchronized + @set:Synchronized + var accountNumber = 0 + + @get:Synchronized + @set:Synchronized + var isNotifySync = false + + @get:Synchronized + @set:Synchronized + var sortType: SortType = SortType.SORT_DATE + + private val sortAscending: MutableMap = mutableMapOf() + + @get:Synchronized + @set:Synchronized + var showPictures = ShowPictures.NEVER + + @get:Synchronized + @set:Synchronized + var isSignatureBeforeQuotedText = false + + @get:Synchronized + @set:Synchronized + var expungePolicy = Expunge.EXPUNGE_IMMEDIATELY + + @get:Synchronized + @set:Synchronized + var maxPushFolders = 0 + + @get:Synchronized + @set:Synchronized + var idleRefreshMinutes = 0 + + private val compressionMap: MutableMap = ConcurrentHashMap() + + @get:Synchronized + @set:Synchronized + var searchableFolders = Searchable.ALL + + @get:Synchronized + @set:Synchronized + var isSubscribedFoldersOnly = false + + @get:Synchronized + @set:Synchronized + var maximumPolledMessageAge = 0 + + @get:Synchronized + @set:Synchronized + var maximumAutoDownloadMessageSize = 0 + + @get:Synchronized + @set:Synchronized + var messageFormat = MessageFormat.HTML + + @get:Synchronized + @set:Synchronized + var isMessageFormatAuto = false + + @get:Synchronized + @set:Synchronized + var isMessageReadReceipt = false + + @get:Synchronized + @set:Synchronized + var quoteStyle = QuoteStyle.PREFIX + + @get:Synchronized + @set:Synchronized + var quotePrefix: String? = null + + @get:Synchronized + @set:Synchronized + var isDefaultQuotedTextShown = false + + @get:Synchronized + @set:Synchronized + var isReplyAfterQuote = false + + @get:Synchronized + @set:Synchronized + var isStripSignature = false + + @get:Synchronized + @set:Synchronized + var isSyncRemoteDeletions = false + + @get:Synchronized + @set:Synchronized + var openPgpProvider: String? = null + set(value) { + field = value?.takeIf { it.isNotEmpty() } + } + + @get:Synchronized + @set:Synchronized + var openPgpKey: Long = 0 + + @get:Synchronized + @set:Synchronized + var autocryptPreferEncryptMutual = false + + @get:Synchronized + @set:Synchronized + var isOpenPgpHideSignOnly = false + + @get:Synchronized + @set:Synchronized + var isOpenPgpEncryptSubject = false + + @get:Synchronized + @set:Synchronized + var isOpenPgpEncryptAllDrafts = false + + @get:Synchronized + @set:Synchronized + var isMarkMessageAsReadOnView = false + + @get:Synchronized + @set:Synchronized + var isMarkMessageAsReadOnDelete = false + + @get:Synchronized + @set:Synchronized + var isAlwaysShowCcBcc = false + + // Temporarily disabled + @get:Synchronized + @set:Synchronized + var isRemoteSearchFullText = false + get() = false + + @get:Synchronized + @set:Synchronized + var remoteSearchNumResults = 0 + set(value) { + field = value.coerceAtLeast(0) + } + + @get:Synchronized + @set:Synchronized + var isUploadSentMessages = false + + @get:Synchronized + @set:Synchronized + var lastSyncTime: Long = 0 + + @get:Synchronized + @set:Synchronized + var lastFolderListRefreshTime: Long = 0 + + @get:Synchronized + var isFinishedSetup = false + private set + + @get:Synchronized + @set:Synchronized + var messagesNotificationChannelVersion = 0 + + @get:Synchronized + @set:Synchronized + var isChangedVisibleLimits = false + private set + + /** + * Database ID of the folder that was last selected for a copy or move operation. + * + * Note: For now this value isn't persisted. So it will be reset when K-9 Mail is restarted. + */ + @get:Synchronized + var lastSelectedFolderId: Long? = null + private set + + @get:Synchronized + @set:Synchronized + var identities: MutableList = mutableListOf() + set(value) { + field = value.toMutableList() + } + + @get:Synchronized + var notificationSettings = NotificationSettings() + private set + + val displayName: String + get() = name ?: email + + @get:Synchronized + @set:Synchronized + override var email: String + get() = identities[0].email!! + set(email) { + val newIdentity = identities[0].withEmail(email) + identities[0] = newIdentity + } + + @get:Synchronized + @set:Synchronized + var senderName: String? + get() = identities[0].name + set(name) { + val newIdentity = identities[0].withName(name) + identities[0] = newIdentity + } + + @get:Synchronized + @set:Synchronized + var signatureUse: Boolean + get() = identities[0].signatureUse + set(signatureUse) { + val newIdentity = identities[0].withSignatureUse(signatureUse) + identities[0] = newIdentity + } + + @get:Synchronized + @set:Synchronized + var signature: String? + get() = identities[0].signature + set(signature) { + val newIdentity = identities[0].withSignature(signature) + identities[0] = newIdentity + } + + /** + * @param automaticCheckIntervalMinutes or -1 for never. + */ + @Synchronized + fun updateAutomaticCheckIntervalMinutes(automaticCheckIntervalMinutes: Int): Boolean { + val oldInterval = this.automaticCheckIntervalMinutes + this.automaticCheckIntervalMinutes = automaticCheckIntervalMinutes + + return oldInterval != automaticCheckIntervalMinutes + } + + @Synchronized + fun setDraftsFolderId(folderId: Long?, selection: SpecialFolderSelection) { + draftsFolderId = folderId + draftsFolderSelection = selection + } + + @Synchronized + fun hasDraftsFolder(): Boolean { + return draftsFolderId != null + } + + @Synchronized + fun setSentFolderId(folderId: Long?, selection: SpecialFolderSelection) { + sentFolderId = folderId + sentFolderSelection = selection + } + + @Synchronized + fun hasSentFolder(): Boolean { + return sentFolderId != null + } + + @Synchronized + fun setTrashFolderId(folderId: Long?, selection: SpecialFolderSelection) { + trashFolderId = folderId + trashFolderSelection = selection + } + + @Synchronized + fun hasTrashFolder(): Boolean { + return trashFolderId != null + } + + @Synchronized + fun setArchiveFolderId(folderId: Long?, selection: SpecialFolderSelection) { + archiveFolderId = folderId + archiveFolderSelection = selection + } + + @Synchronized + fun hasArchiveFolder(): Boolean { + return archiveFolderId != null + } + + @Synchronized + fun setSpamFolderId(folderId: Long?, selection: SpecialFolderSelection) { + spamFolderId = folderId + spamFolderSelection = selection + } + + @Synchronized + fun hasSpamFolder(): Boolean { + return spamFolderId != null + } + + @Synchronized + fun updateFolderSyncMode(syncMode: FolderMode): Boolean { + val oldSyncMode = folderSyncMode + folderSyncMode = syncMode + + return (oldSyncMode == FolderMode.NONE && syncMode != FolderMode.NONE) || + (oldSyncMode != FolderMode.NONE && syncMode == FolderMode.NONE) + } + + @Synchronized + fun incrementMessagesNotificationChannelVersion() { + messagesNotificationChannelVersion++ + } + + @Synchronized + fun isSortAscending(sortType: SortType): Boolean { + return sortAscending.getOrPut(sortType) { sortType.isDefaultAscending } + } + + @Synchronized + fun setSortAscending(sortType: SortType, sortAscending: Boolean) { + this.sortAscending[sortType] = sortAscending + } + + @Synchronized + fun setCompression(networkType: NetworkType, useCompression: Boolean) { + compressionMap[networkType] = useCompression + } + + @Synchronized + fun useCompression(networkType: NetworkType): Boolean { + return compressionMap[networkType] ?: return true + } + + fun getCompressionMap(): Map { + return compressionMap.toMap() + } + + @Synchronized + fun replaceIdentities(identities: List) { + this.identities = identities.toMutableList() + } + + @Synchronized + fun getIdentity(index: Int): Identity { + if (index !in identities.indices) error("Identity with index $index not found") + + return identities[index] + } + + fun isAnIdentity(addresses: Array
?): Boolean { + if (addresses == null) return false + + return addresses.any { address -> isAnIdentity(address) } + } + + fun isAnIdentity(address: Address): Boolean { + return findIdentity(address) != null + } + + @Synchronized + fun findIdentity(address: Address): Identity? { + return identities.find { identity -> + identity.email.equals(address.address, ignoreCase = true) + } + } + + val earliestPollDate: Date? + get() { + val age = maximumPolledMessageAge.takeIf { it >= 0 } ?: return null + + val now = Calendar.getInstance() + now[Calendar.HOUR_OF_DAY] = 0 + now[Calendar.MINUTE] = 0 + now[Calendar.SECOND] = 0 + now[Calendar.MILLISECOND] = 0 + + if (age < 28) { + now.add(Calendar.DATE, age * -1) + } else when (age) { + 28 -> now.add(Calendar.MONTH, -1) + 56 -> now.add(Calendar.MONTH, -2) + 84 -> now.add(Calendar.MONTH, -3) + 168 -> now.add(Calendar.MONTH, -6) + 365 -> now.add(Calendar.YEAR, -1) + } + + return now.time + } + + val isOpenPgpProviderConfigured: Boolean + get() = openPgpProvider != null + + @Synchronized + fun hasOpenPgpKey(): Boolean { + return openPgpKey != NO_OPENPGP_KEY + } + + @Synchronized + fun setLastSelectedFolderId(folderId: Long) { + lastSelectedFolderId = folderId + } + + @Synchronized + fun resetChangeMarkers() { + isChangedVisibleLimits = false + } + + @Synchronized + fun markSetupFinished() { + isFinishedSetup = true + } + + @Synchronized + fun updateNotificationSettings(block: (oldNotificationSettings: NotificationSettings) -> NotificationSettings) { + notificationSettings = block(notificationSettings) + } + + override fun toString(): String { + return if (K9.isSensitiveDebugLoggingEnabled) displayName else uuid + } + + override fun equals(other: Any?): Boolean { + return if (other is Account) { + other.uuid == uuid + } else { + super.equals(other) + } + } + + override fun hashCode(): Int { + return uuid.hashCode() + } + + enum class FolderMode { + NONE, + ALL, + FIRST_CLASS, + FIRST_AND_SECOND_CLASS, + NOT_SECOND_CLASS + } + + enum class SpecialFolderSelection { + AUTOMATIC, + MANUAL + } + + enum class ShowPictures { + NEVER, + ALWAYS, + ONLY_FROM_CONTACTS + } + + enum class Searchable { + ALL, + DISPLAYABLE, + NONE + } + + enum class QuoteStyle { + PREFIX, + HEADER + } + + enum class MessageFormat { + TEXT, + HTML, + AUTO + } + + enum class Expunge { + EXPUNGE_IMMEDIATELY, + EXPUNGE_MANUALLY, + EXPUNGE_ON_POLL; + + fun toBackendExpungePolicy(): ExpungePolicy = when (this) { + EXPUNGE_IMMEDIATELY -> ExpungePolicy.IMMEDIATELY + EXPUNGE_MANUALLY -> ExpungePolicy.MANUALLY + EXPUNGE_ON_POLL -> ExpungePolicy.ON_POLL + } + } + + enum class DeletePolicy(@JvmField val setting: Int) { + NEVER(0), + SEVEN_DAYS(1), + ON_DELETE(2), + MARK_AS_READ(3); + + companion object { + fun fromInt(initialSetting: Int): DeletePolicy { + return values().find { it.setting == initialSetting } ?: error("DeletePolicy $initialSetting unknown") + } + } + } + + enum class SortType(val isDefaultAscending: Boolean) { + SORT_DATE(false), + SORT_ARRIVAL(false), + SORT_SUBJECT(true), + SORT_SENDER(true), + SORT_UNREAD(true), + SORT_FLAGGED(true), + SORT_ATTACHMENT(true); + } + + companion object { + /** + * Fixed name of outbox - not actually displayed. + */ + const val OUTBOX_NAME = "Outbox" + + @JvmField + val DEFAULT_SORT_TYPE = SortType.SORT_DATE + const val DEFAULT_SORT_ASCENDING = false + const val NO_OPENPGP_KEY: Long = 0 + const val UNASSIGNED_ACCOUNT_NUMBER = -1 + const val INTERVAL_MINUTES_NEVER = -1 + const val DEFAULT_SYNC_INTERVAL = 60 + } +} 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 4b94a99c0b9bf8f7c37b24f986eb67eb2b67a60f..4052fd25482eaafb3c1e7f1cf18c4b4474e15410 100644 --- a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt +++ b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt @@ -1,19 +1,19 @@ package com.fsck.k9 -import com.fsck.k9.Account.DEFAULT_SORT_ASCENDING -import com.fsck.k9.Account.DEFAULT_SORT_TYPE -import com.fsck.k9.Account.DEFAULT_SYNC_INTERVAL +import com.fsck.k9.Account.Companion.DEFAULT_SORT_ASCENDING +import com.fsck.k9.Account.Companion.DEFAULT_SORT_TYPE +import com.fsck.k9.Account.Companion.DEFAULT_SYNC_INTERVAL +import com.fsck.k9.Account.Companion.NO_OPENPGP_KEY +import com.fsck.k9.Account.Companion.UNASSIGNED_ACCOUNT_NUMBER import com.fsck.k9.Account.DeletePolicy import com.fsck.k9.Account.Expunge import com.fsck.k9.Account.FolderMode import com.fsck.k9.Account.MessageFormat -import com.fsck.k9.Account.NO_OPENPGP_KEY import com.fsck.k9.Account.QuoteStyle import com.fsck.k9.Account.Searchable import com.fsck.k9.Account.ShowPictures import com.fsck.k9.Account.SortType import com.fsck.k9.Account.SpecialFolderSelection -import com.fsck.k9.Account.UNASSIGNED_ACCOUNT_NUMBER import com.fsck.k9.helper.Utility import com.fsck.k9.mail.NetworkType import com.fsck.k9.mailstore.StorageManager @@ -38,7 +38,7 @@ class AccountPreferenceSerializer( storage.getString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", "") ) localStorageProviderId = storage.getString("$accountUuid.localStorageProvider", storageManager.defaultProviderId) - description = storage.getString("$accountUuid.description", null) + name = storage.getString("$accountUuid.description", null) alwaysBcc = storage.getString("$accountUuid.alwaysBcc", alwaysBcc) automaticCheckIntervalMinutes = storage.getInt("$accountUuid.automaticCheckIntervalMinutes", DEFAULT_SYNC_INTERVAL) idleRefreshMinutes = storage.getInt("$accountUuid.idleRefreshMinutes", 24) @@ -137,16 +137,16 @@ class AccountPreferenceSerializer( showPictures = getEnumStringPref(storage, "$accountUuid.showPicturesEnum", ShowPictures.NEVER) - notificationSetting.isVibrateEnabled = storage.getBoolean("$accountUuid.vibrate", false) - notificationSetting.vibratePattern = storage.getInt("$accountUuid.vibratePattern", 0) - notificationSetting.vibrateTimes = storage.getInt("$accountUuid.vibrateTimes", 5) - notificationSetting.isRingEnabled = storage.getBoolean("$accountUuid.ring", true) - notificationSetting.ringtone = storage.getString( - "$accountUuid.ringtone", - "content://settings/system/notification_sound" - ) - notificationSetting.setLed(storage.getBoolean("$accountUuid.led", true)) - notificationSetting.ledColor = storage.getInt("$accountUuid.ledColor", chipColor) + updateNotificationSettings { + NotificationSettings( + isRingEnabled = storage.getBoolean("$accountUuid.ring", true), + ringtone = storage.getString("$accountUuid.ringtone", DEFAULT_RINGTONE_URI), + light = getEnumStringPref(storage, "$accountUuid.notificationLight", NotificationLight.Disabled), + isVibrateEnabled = storage.getBoolean("$accountUuid.vibrate", false), + vibratePattern = VibratePattern.deserialize(storage.getInt("$accountUuid.vibratePattern", 0)), + vibrateTimes = storage.getInt("$accountUuid.vibrateTimes", 5) + ) + } folderDisplayMode = getEnumStringPref(storage, "$accountUuid.folderDisplayMode", FolderMode.NOT_SECOND_CLASS) @@ -159,7 +159,7 @@ class AccountPreferenceSerializer( searchableFolders = getEnumStringPref(storage, "$accountUuid.searchableFolders", Searchable.ALL) isSignatureBeforeQuotedText = storage.getBoolean("$accountUuid.signatureBeforeQuotedText", false) - identities = loadIdentities(accountUuid, storage) + replaceIdentities(loadIdentities(accountUuid, storage)) openPgpProvider = storage.getString("$accountUuid.openPgpProvider", "") openPgpKey = storage.getLong("$accountUuid.cryptoKey", NO_OPENPGP_KEY) @@ -167,7 +167,6 @@ class AccountPreferenceSerializer( isOpenPgpEncryptSubject = storage.getBoolean("$accountUuid.openPgpEncryptSubject", true) isOpenPgpEncryptAllDrafts = storage.getBoolean("$accountUuid.openPgpEncryptAllDrafts", true) autocryptPreferEncryptMutual = storage.getBoolean("$accountUuid.autocryptMutualMode", false) - isAllowRemoteSearch = storage.getBoolean("$accountUuid.allowRemoteSearch", false) isRemoteSearchFullText = storage.getBoolean("$accountUuid.remoteSearchFullText", false) remoteSearchNumResults = storage.getInt("$accountUuid.remoteSearchNumResults", DEFAULT_REMOTE_SEARCH_NUM_RESULTS) isUploadSentMessages = storage.getBoolean("$accountUuid.uploadSentMessages", true) @@ -180,11 +179,6 @@ class AccountPreferenceSerializer( val isFinishedSetup = storage.getBoolean("$accountUuid.isFinishedSetup", true) if (isFinishedSetup) markSetupFinished() - // Use email address as account description if necessary - if (description == null) { - description = email - } - resetChangeMarkers() } } @@ -249,7 +243,7 @@ class AccountPreferenceSerializer( editor.putString("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(incomingServerSettings)) editor.putString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(outgoingServerSettings)) editor.putString("$accountUuid.localStorageProvider", localStorageProviderId) - editor.putString("$accountUuid.description", description) + editor.putString("$accountUuid.description", name) editor.putString("$accountUuid.alwaysBcc", alwaysBcc) editor.putInt("$accountUuid.automaticCheckIntervalMinutes", automaticCheckIntervalMinutes) editor.putInt("$accountUuid.idleRefreshMinutes", idleRefreshMinutes) @@ -321,7 +315,6 @@ class AccountPreferenceSerializer( editor.putBoolean("$accountUuid.openPgpEncryptAllDrafts", isOpenPgpEncryptAllDrafts) editor.putString("$accountUuid.openPgpProvider", openPgpProvider) editor.putBoolean("$accountUuid.autocryptMutualMode", autocryptPreferEncryptMutual) - editor.putBoolean("$accountUuid.allowRemoteSearch", isAllowRemoteSearch) editor.putBoolean("$accountUuid.remoteSearchFullText", isRemoteSearchFullText) editor.putInt("$accountUuid.remoteSearchNumResults", remoteSearchNumResults) editor.putBoolean("$accountUuid.uploadSentMessages", isUploadSentMessages) @@ -329,17 +322,17 @@ class AccountPreferenceSerializer( editor.putBoolean("$accountUuid.markMessageAsReadOnDelete", isMarkMessageAsReadOnDelete) editor.putBoolean("$accountUuid.alwaysShowCcBcc", isAlwaysShowCcBcc) - editor.putBoolean("$accountUuid.vibrate", notificationSetting.isVibrateEnabled) - editor.putInt("$accountUuid.vibratePattern", notificationSetting.vibratePattern) - editor.putInt("$accountUuid.vibrateTimes", notificationSetting.vibrateTimes) - editor.putBoolean("$accountUuid.ring", notificationSetting.isRingEnabled) - editor.putString("$accountUuid.ringtone", notificationSetting.ringtone) - editor.putBoolean("$accountUuid.led", notificationSetting.isLedEnabled) - editor.putInt("$accountUuid.ledColor", notificationSetting.ledColor) + editor.putBoolean("$accountUuid.vibrate", notificationSettings.isVibrateEnabled) + editor.putInt("$accountUuid.vibratePattern", notificationSettings.vibratePattern.serialize()) + editor.putInt("$accountUuid.vibrateTimes", notificationSettings.vibrateTimes) + editor.putBoolean("$accountUuid.ring", notificationSettings.isRingEnabled) + editor.putString("$accountUuid.ringtone", notificationSettings.ringtone) + editor.putString("$accountUuid.notificationLight", notificationSettings.light.name) editor.putLong("$accountUuid.lastSyncTime", lastSyncTime) editor.putLong("$accountUuid.lastFolderListRefreshTime", lastFolderListRefreshTime) editor.putBoolean("$accountUuid.isFinishedSetup", isFinishedSetup) + val compressionMap = getCompressionMap() for (type in NetworkType.values()) { val useCompression = compressionMap[type] if (useCompression != null) { @@ -413,8 +406,7 @@ class AccountPreferenceSerializer( editor.remove("$accountUuid.maxPushFolders") editor.remove("$accountUuid.searchableFolders") editor.remove("$accountUuid.chipColor") - editor.remove("$accountUuid.led") - editor.remove("$accountUuid.ledColor") + editor.remove("$accountUuid.notificationLight") editor.remove("$accountUuid.subscribedFoldersOnly") editor.remove("$accountUuid.maximumPolledMessageAge") editor.remove("$accountUuid.maximumAutoDownloadMessageSize") @@ -441,7 +433,6 @@ class AccountPreferenceSerializer( editor.remove("$accountUuid.markMessageAsReadOnView") editor.remove("$accountUuid.markMessageAsReadOnDelete") editor.remove("$accountUuid.alwaysShowCcBcc") - editor.remove("$accountUuid.allowRemoteSearch") editor.remove("$accountUuid.remoteSearchFullText") editor.remove("$accountUuid.remoteSearchNumResults") editor.remove("$accountUuid.uploadSentMessages") @@ -584,7 +575,6 @@ class AccountPreferenceSerializer( isStripSignature = DEFAULT_STRIP_SIGNATURE isSyncRemoteDeletions = true openPgpKey = NO_OPENPGP_KEY - isAllowRemoteSearch = false isRemoteSearchFullText = false remoteSearchNumResults = DEFAULT_REMOTE_SEARCH_NUM_RESULTS isUploadSentMessages = true @@ -612,13 +602,15 @@ class AccountPreferenceSerializer( ) identities.add(identity) - with(notificationSetting) { - isVibrateEnabled = false - vibratePattern = 0 - vibrateTimes = 5 - isRingEnabled = true - ringtone = "content://settings/system/notification_sound" - ledColor = chipColor + updateNotificationSettings { + NotificationSettings( + isRingEnabled = true, + ringtone = DEFAULT_RINGTONE_URI, + light = NotificationLight.Disabled, + isVibrateEnabled = false, + vibratePattern = VibratePattern.Default, + vibrateTimes = 5 + ) } resetChangeMarkers() @@ -648,5 +640,6 @@ class AccountPreferenceSerializer( const val DEFAULT_REPLY_AFTER_QUOTE = false const val DEFAULT_STRIP_SIGNATURE = true const val DEFAULT_REMOTE_SEARCH_NUM_RESULTS = 25 + const val DEFAULT_RINGTONE_URI = "content://settings/system/notification_sound" } } diff --git a/app/core/src/main/java/com/fsck/k9/BaseAccount.java b/app/core/src/main/java/com/fsck/k9/BaseAccount.java deleted file mode 100644 index a2c1ad0e17527ad3f120ffeabd0742bc70dfca5a..0000000000000000000000000000000000000000 --- a/app/core/src/main/java/com/fsck/k9/BaseAccount.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.fsck.k9; - -public interface BaseAccount { - String getEmail(); - String getDescription(); - String getUuid(); -} diff --git a/app/core/src/main/java/com/fsck/k9/BaseAccount.kt b/app/core/src/main/java/com/fsck/k9/BaseAccount.kt new file mode 100644 index 0000000000000000000000000000000000000000..330418badf69240af9ce32d4e1448d6b31af3281 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/BaseAccount.kt @@ -0,0 +1,7 @@ +package com.fsck.k9 + +interface BaseAccount { + val uuid: String + val name: String? + val email: String +} 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 b4cd2e65bdab6f7144585bf4ddf54f5eeeba965d..53baf43f6ca3679f518a242a59078f137d36f621 100644 --- a/app/core/src/main/java/com/fsck/k9/K9.kt +++ b/app/core/src/main/java/com/fsck/k9/K9.kt @@ -184,7 +184,7 @@ object K9 : EarlyInit { var isChangeContactNameColor = false @JvmStatic - var contactNameColor = 0xff00008f.toInt() + var contactNameColor = 0xFF1093F5.toInt() @JvmStatic var isShowContactPicture = true @@ -333,7 +333,7 @@ object K9 : EarlyInit { isShowContactName = storage.getBoolean("showContactName", false) isShowContactPicture = storage.getBoolean("showContactPicture", true) isChangeContactNameColor = storage.getBoolean("changeRegisteredNameColor", false) - contactNameColor = storage.getInt("registeredNameColor", -0xffff71) + contactNameColor = storage.getInt("registeredNameColor", 0xFF1093F5.toInt()) isUseMessageViewFixedWidthFont = storage.getBoolean("messageViewFixedWidthFont", false) isMessageViewReturnToList = storage.getBoolean("messageViewReturnToList", false) isMessageViewShowNext = storage.getBoolean("messageViewShowNext", false) diff --git a/app/core/src/main/java/com/fsck/k9/NotificationLight.kt b/app/core/src/main/java/com/fsck/k9/NotificationLight.kt new file mode 100644 index 0000000000000000000000000000000000000000..4a6f36790ebae6d7a295e85dbfbb2bfb914574f6 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/NotificationLight.kt @@ -0,0 +1,33 @@ +package com.fsck.k9 + +import android.app.Notification + +enum class NotificationLight { + Disabled, + AccountColor, + SystemDefaultColor, + White, + Red, + Green, + Blue, + Yellow, + Cyan, + Magenta; + + fun toColor(account: Account): Int? { + return when (this) { + Disabled -> null + AccountColor -> account.chipColor.toArgb() + SystemDefaultColor -> Notification.COLOR_DEFAULT + White -> 0xFFFFFF.toArgb() + Red -> 0xFF0000.toArgb() + Green -> 0x00FF00.toArgb() + Blue -> 0x0000FF.toArgb() + Yellow -> 0xFFFF00.toArgb() + Cyan -> 0x00FFFF.toArgb() + Magenta -> 0xFF00FF.toArgb() + } + } + + private fun Int.toArgb() = this or 0xFF000000L.toInt() +} diff --git a/app/core/src/main/java/com/fsck/k9/NotificationSetting.java b/app/core/src/main/java/com/fsck/k9/NotificationSetting.java deleted file mode 100644 index c9d69f2499b54d7bc847f70779c1e16cc2f448d0..0000000000000000000000000000000000000000 --- a/app/core/src/main/java/com/fsck/k9/NotificationSetting.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.fsck.k9; - -/** - * Describes how a notification should behave. - */ -public class NotificationSetting { - private boolean ringEnabled; - private String ringtoneUri; - - private boolean ledEnabled; - private int ledColor; - - private boolean vibrateEnabled; - - private int vibratePattern; - private int vibrateTimes; - - /** - * Set the ringtone kill switch. Allow to disable ringtone without losing - * ringtone selection. - * - * @param ringEnabled - * true to allow ringtones, false - * otherwise. - */ - public synchronized void setRingEnabled(boolean ringEnabled) { - this.ringEnabled = ringEnabled; - } - - /** - * @return true if ringtone is allowed to play, - * false otherwise. - */ - public synchronized boolean isRingEnabled() { - return ringEnabled; - } - - public synchronized String getRingtone() { - return ringtoneUri; - } - - public synchronized void setRingtone(String ringtoneUri) { - this.ringtoneUri = ringtoneUri; - } - - public synchronized boolean isLedEnabled() { - return ledEnabled; - } - - public synchronized void setLed(final boolean led) { - ledEnabled = led; - } - - public synchronized int getLedColor() { - return ledColor; - } - - public synchronized void setLedColor(int color) { - ledColor = color; - } - - public synchronized boolean isVibrateEnabled() { - return vibrateEnabled; - } - - public synchronized void setVibrateEnabled(boolean vibrate) { - vibrateEnabled = vibrate; - } - - public synchronized int getVibratePattern() { - return vibratePattern; - } - - public synchronized int getVibrateTimes() { - return vibrateTimes; - } - - public synchronized void setVibratePattern(int pattern) { - vibratePattern = pattern; - } - - public synchronized void setVibrateTimes(int times) { - vibrateTimes = times; - } - - - - /* - * Fetch a vibration pattern. - * - * @param vibratePattern Vibration pattern index to use. - * @param vibrateTimes Number of times to do the vibration pattern. - * @return Pattern multiplied by the number of times requested. - */ - - public long[] getVibration() { - return getVibration(vibratePattern, vibrateTimes); - } - - public static long[] getVibration(int pattern, int times) { - // These are "off, on" patterns, specified in milliseconds - long[] pattern0 = new long[] {300, 200}; // like the default pattern - long[] pattern1 = new long[] {100, 200}; - long[] pattern2 = new long[] {100, 500}; - long[] pattern3 = new long[] {200, 200}; - long[] pattern4 = new long[] {200, 500}; - long[] pattern5 = new long[] {500, 500}; - - long[] selectedPattern = pattern0; //default pattern - - switch (pattern) { - case 1: - selectedPattern = pattern1; - break; - case 2: - selectedPattern = pattern2; - break; - case 3: - selectedPattern = pattern3; - break; - case 4: - selectedPattern = pattern4; - break; - case 5: - selectedPattern = pattern5; - break; - } - - long[] repeatedPattern = new long[selectedPattern.length * times]; - for (int n = 0; n < times; n++) { - System.arraycopy(selectedPattern, 0, repeatedPattern, n * selectedPattern.length, selectedPattern.length); - } - // Do not wait before starting the vibration pattern. - repeatedPattern[0] = 0; - return repeatedPattern; - } - -} diff --git a/app/core/src/main/java/com/fsck/k9/NotificationSettings.kt b/app/core/src/main/java/com/fsck/k9/NotificationSettings.kt new file mode 100644 index 0000000000000000000000000000000000000000..b94eec51b8ecfcad33e70900d2e005533b063876 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/NotificationSettings.kt @@ -0,0 +1,66 @@ +package com.fsck.k9 + +/** + * Describes how a notification should behave. + */ +data class NotificationSettings( + val isRingEnabled: Boolean = false, + val ringtone: String? = null, + val light: NotificationLight = NotificationLight.Disabled, + val isVibrateEnabled: Boolean = false, + val vibratePattern: VibratePattern = VibratePattern.Default, + val vibrateTimes: Int = 0 +) { + val vibrationPattern: LongArray + get() = getVibrationPattern(vibratePattern, vibrateTimes) + + companion object { + fun getVibrationPattern(vibratePattern: VibratePattern, times: Int): LongArray { + val selectedPattern = vibratePattern.vibrationPattern + val repeatedPattern = LongArray(selectedPattern.size * times) + for (n in 0 until times) { + System.arraycopy(selectedPattern, 0, repeatedPattern, n * selectedPattern.size, selectedPattern.size) + } + + // Do not wait before starting the vibration pattern. + repeatedPattern[0] = 0 + + return repeatedPattern + } + } +} + +enum class VibratePattern( + /** + * These are "off, on" patterns, specified in milliseconds. + */ + val vibrationPattern: LongArray +) { + Default(vibrationPattern = longArrayOf(300, 200)), + Pattern1(vibrationPattern = longArrayOf(100, 200)), + Pattern2(vibrationPattern = longArrayOf(100, 500)), + Pattern3(vibrationPattern = longArrayOf(200, 200)), + Pattern4(vibrationPattern = longArrayOf(200, 500)), + Pattern5(vibrationPattern = longArrayOf(500, 500)); + + fun serialize(): Int = when (this) { + Default -> 0 + Pattern1 -> 1 + Pattern2 -> 2 + Pattern3 -> 3 + Pattern4 -> 4 + Pattern5 -> 5 + } + + companion object { + fun deserialize(value: Int): VibratePattern = when (value) { + 0 -> Default + 1 -> Pattern1 + 2 -> Pattern2 + 3 -> Pattern3 + 4 -> Pattern4 + 5 -> Pattern5 + else -> error("Unknown VibratePattern value: $value") + } + } +} 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 18441845ba19456be441f16435ff1a5db98eb7bb..f7eaab83d92a56e823589467f1bcdea0aeb6e700 100644 --- a/app/core/src/main/java/com/fsck/k9/Preferences.kt +++ b/app/core/src/main/java/com/fsck/k9/Preferences.kt @@ -32,6 +32,7 @@ class Preferences internal constructor( private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO ) : AccountManager { private val accountLock = Any() + private val storageLock = Any() @GuardedBy("accountLock") private var accountsMap: MutableMap? = null @@ -44,15 +45,22 @@ class Preferences internal constructor( private val accountsChangeListeners = CopyOnWriteArraySet() private val accountRemovedListeners = CopyOnWriteArraySet() - val storage = Storage() + @GuardedBy("storageLock") + private var currentStorage: Storage? = null - init { - val persistedStorageValues = storagePersister.loadValues() - storage.replaceAll(persistedStorageValues) - } + val storage: Storage + get() = synchronized(storageLock) { + currentStorage ?: storagePersister.loadValues().also { newStorage -> + currentStorage = newStorage + } + } fun createStorageEditor(): StorageEditor { - return storagePersister.createStorageEditor(storage) + return storagePersister.createStorageEditor { updater -> + synchronized(storageLock) { + currentStorage = updater(storage) + } + } } @RestrictTo(RestrictTo.Scope.TESTS) @@ -197,9 +205,11 @@ class Preferences internal constructor( ensureAssignedAccountNumber(account) processChangedValues(account) - val editor = createStorageEditor() - accountPreferenceSerializer.save(editor, storage, account) - editor.commit() + synchronized(accountLock) { + val editor = createStorageEditor() + accountPreferenceSerializer.save(editor, storage, account) + editor.commit() + } notifyAccountsChangeListeners() } diff --git a/app/core/src/main/java/com/fsck/k9/StrictMode.kt b/app/core/src/main/java/com/fsck/k9/StrictMode.kt index 93fd3521294a9134dd9a5dba3f290a70315e1807..29c628686249aab18b7b3729185b733fedc42810 100644 --- a/app/core/src/main/java/com/fsck/k9/StrictMode.kt +++ b/app/core/src/main/java/com/fsck/k9/StrictMode.kt @@ -34,6 +34,10 @@ private fun createVmPolicy(): VmPolicy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { detectCredentialProtectedWhileLocked() } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + detectIncorrectContextUse() + detectUnsafeIntentLaunch() + } } .penaltyLog() .build() 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 f3e94011fbc368b326c5537af49486b671ef5710..893ef7ab3665f857ef6387977c2596977d08c9d7 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 @@ -6,6 +6,7 @@ import com.fsck.k9.backend.BackendManager import com.fsck.k9.mailstore.LocalStoreProvider import com.fsck.k9.mailstore.MessageStoreManager import com.fsck.k9.mailstore.SaveMessageDataCreator +import com.fsck.k9.mailstore.SpecialLocalFoldersCreator import com.fsck.k9.notification.NotificationController import com.fsck.k9.notification.NotificationStrategy import org.koin.core.qualifier.named @@ -23,6 +24,7 @@ val controllerModule = module { get(), get(), get(), + get(), get(named("controllerExtensions")) ) } 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 8b430664496933eb4e31074cee7546f271d23702..21633eef31be19422e56321ae303f573805fbd69 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 @@ -78,6 +78,7 @@ 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.SpecialLocalFoldersCreator; import com.fsck.k9.notification.NotificationController; import com.fsck.k9.notification.NotificationStrategy; import com.fsck.k9.search.LocalSearch; @@ -118,6 +119,7 @@ public class MessagingController { private final Preferences preferences; private final MessageStoreManager messageStoreManager; private final SaveMessageDataCreator saveMessageDataCreator; + private final SpecialLocalFoldersCreator specialLocalFoldersCreator; private final Thread controllerThread; @@ -143,7 +145,8 @@ public class MessagingController { NotificationStrategy notificationStrategy, LocalStoreProvider localStoreProvider, MessageCountsProvider messageCountsProvider, BackendManager backendManager, Preferences preferences, MessageStoreManager messageStoreManager, - SaveMessageDataCreator saveMessageDataCreator, List controllerExtensions) { + SaveMessageDataCreator saveMessageDataCreator, SpecialLocalFoldersCreator specialLocalFoldersCreator, + List controllerExtensions) { this.context = context; this.notificationController = notificationController; this.notificationStrategy = notificationStrategy; @@ -153,6 +156,7 @@ public class MessagingController { this.preferences = preferences; this.messageStoreManager = messageStoreManager; this.saveMessageDataCreator = saveMessageDataCreator; + this.specialLocalFoldersCreator = specialLocalFoldersCreator; controllerThread = new Thread(new Runnable() { @Override @@ -263,7 +267,7 @@ public class MessagingController { try { return localStoreProvider.getInstance(account); } catch (MessagingException e) { - throw new IllegalStateException("Couldn't get LocalStore for account " + account.getDescription()); + throw new IllegalStateException("Couldn't get LocalStore for account " + account); } } @@ -676,7 +680,7 @@ public class MessagingController { if (commandException != null && !syncListener.syncFailed) { String rootMessage = getRootCauseMessage(commandException); - Timber.e("Root cause failure in %s:%s was '%s'", account.getDescription(), folderServerId, rootMessage); + Timber.e("Root cause failure in %s:%s was '%s'", account, folderServerId, rootMessage); updateFolderStatus(account, folderServerId, rootMessage); listener.synchronizeMailboxFailed(account, folderId, rootMessage); } @@ -946,7 +950,7 @@ public class MessagingController { if (operation != MoveOrCopyFlavor.COPY) { if (backend.getSupportsExpunge() && account.getExpungePolicy() == Expunge.EXPUNGE_IMMEDIATELY) { - Timber.i("processingPendingMoveOrCopy expunging folder %s:%s", account.getDescription(), srcFolderServerId); + Timber.i("processingPendingMoveOrCopy expunging folder %s:%s", account, srcFolderServerId); backend.expungeMessages(srcFolderServerId, uids); } @@ -1163,6 +1167,10 @@ public class MessagingController { l.folderStatusChanged(account, folderId); } + if (flag == Flag.SEEN && newState) { + cancelNotificationsForMessages(account, folderId, uids); + } + if (accountSupportsFlags) { LocalFolder localFolder = localStore.getFolder(folderId); try { @@ -1179,6 +1187,13 @@ public class MessagingController { } } + private void cancelNotificationsForMessages(Account account, long folderId, List uids) { + for (String uid : uids) { + MessageReference messageReference = new MessageReference(account.getUuid(), folderId, uid); + notificationController.removeNewMailNotification(account, messageReference); + } + } + /** * Set or remove a flag for a set of messages in a specific folder. *

@@ -1424,8 +1439,13 @@ public class MessagingController { try { Long outboxFolderId = account.getOutboxFolderId(); if (outboxFolderId == null) { - Timber.e("Error sending message. No Outbox folder configured."); - return; + if (BuildConfig.DEBUG) { + throw new AssertionError("Outbox does not exist"); + } + + Timber.w("Outbox does not exist"); + + outboxFolderId = specialLocalFoldersCreator.createOutbox(account); } message.setFlag(Flag.SEEN, true); @@ -1513,7 +1533,7 @@ public class MessagingController { OutboxStateRepository outboxStateRepository = localStore.getOutboxStateRepository(); LocalFolder localFolder = localStore.getFolder(account.getOutboxFolderId()); if (!localFolder.exists()) { - Timber.v("Outbox does not exist"); + Timber.w("Outbox does not exist"); return; } @@ -2045,6 +2065,8 @@ public class MessagingController { List syncedMessages = new ArrayList<>(); List syncedMessageUids = new ArrayList<>(); for (LocalMessage message : messages) { + notificationController.removeNewMailNotification(account, message.makeMessageReference()); + String uid = message.getUid(); if (uid.startsWith(K9.LOCAL_UID_PREFIX)) { localOnlyMessages.add(message); @@ -2113,7 +2135,7 @@ public class MessagingController { } } - Timber.d("Delete policy for account %s is %s", account.getDescription(), account.getDeletePolicy()); + Timber.d("Delete policy for account %s is %s", account, account.getDeletePolicy()); Long outboxFolderId = account.getOutboxFolderId(); if (outboxFolderId != null && folderId == outboxFolderId && supportsUpload(account)) { @@ -2273,11 +2295,11 @@ public class MessagingController { } }); - Timber.v("performPeriodicMailSync(%s) about to await latch release", account.getDescription()); + Timber.v("performPeriodicMailSync(%s) about to await latch release", account); try { latch.await(); - Timber.v("performPeriodicMailSync(%s) got latch release", account.getDescription()); + Timber.v("performPeriodicMailSync(%s) got latch release", account); } catch (Exception e) { Timber.e(e, "Interrupted while awaiting latch release"); } @@ -2362,7 +2384,7 @@ public class MessagingController { private void checkMailForAccount(final Context context, final Account account, final boolean ignoreLastCheckedTime, final MessagingListener listener) { - Timber.i("Synchronizing account %s", account.getDescription()); + Timber.i("Synchronizing account %s", account); NotificationState notificationState = new NotificationState(); @@ -2407,12 +2429,12 @@ public class MessagingController { synchronizeFolder(account, folder, ignoreLastCheckedTime, listener, notificationState); } } catch (MessagingException e) { - Timber.e(e, "Unable to synchronize account %s", account.getName()); + Timber.e(e, "Unable to synchronize account %s", account); } finally { - putBackground("clear notification flag for " + account.getDescription(), null, new Runnable() { + putBackground("clear notification flag for " + account, null, new Runnable() { @Override public void run() { - Timber.v("Clearing notification flag for %s", account.getDescription()); + Timber.v("Clearing notification flag for %s", account); clearFetchingMailNotification(account); @@ -2463,7 +2485,7 @@ public class MessagingController { showEmptyFetchingMailNotificationIfNecessary(account); } } catch (Exception e) { - Timber.e(e, "Exception while processing folder %s:%s", account.getDescription(), folder.getServerId()); + Timber.e(e, "Exception while processing folder %s:%s", account, folder.getServerId()); } } @@ -2484,7 +2506,7 @@ public class MessagingController { } public void compact(final Account account, final MessagingListener ml) { - putBackground("compact:" + account.getDescription(), ml, new Runnable() { + putBackground("compact:" + account, ml, new Runnable() { @Override public void run() { try { @@ -2496,7 +2518,7 @@ public class MessagingController { l.accountSizeChanged(account, oldSize, newSize); } } catch (Exception e) { - Timber.e(e, "Failed to compact account %s", account.getDescription()); + Timber.e(e, "Failed to compact account %s", account); } } }); diff --git a/app/core/src/main/java/com/fsck/k9/helper/PendingIntentCompat.kt b/app/core/src/main/java/com/fsck/k9/helper/PendingIntentCompat.kt new file mode 100644 index 0000000000000000000000000000000000000000..a88d98e5a77f406b4594c0ae2289e98703195f10 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/helper/PendingIntentCompat.kt @@ -0,0 +1,12 @@ +package com.fsck.k9.helper + +import android.app.PendingIntent +import android.os.Build + +object PendingIntentCompat { + @JvmField + val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0 + + @JvmField + val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0 +} diff --git a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt index 989a42ee0d1c4bfafc0cacd52457e4ed18e7334f..77ea8abbc13369a502ae64c5a8f12d85499f5fdf 100644 --- a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt +++ b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt @@ -16,7 +16,7 @@ import timber.log.Timber class MailSyncWorkerManager(private val workManager: WorkManager, val clock: Clock) { fun cancelMailSync(account: Account) { - Timber.v("Canceling mail sync worker for %s", account.description) + Timber.v("Canceling mail sync worker for %s", account) val uniqueWorkName = createUniqueWorkName(account.uuid) workManager.cancelUniqueWork(uniqueWorkName) } @@ -25,7 +25,7 @@ class MailSyncWorkerManager(private val workManager: WorkManager, val clock: Clo if (isNeverSyncInBackground()) return getSyncIntervalIfEnabled(account)?.let { syncIntervalMinutes -> - Timber.v("Scheduling mail sync worker for %s", account.description) + Timber.v("Scheduling mail sync worker for %s", account) Timber.v(" sync interval: %d minutes", syncIntervalMinutes) val constraints = Constraints.Builder() 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 a3a5cead774b2fca8bfaa04f34ec7c11433b35e9..dc18704e76527a50819bc51d12c3f1db6ebf8e0c 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 @@ -23,7 +23,7 @@ import com.fsck.k9.mail.internet.Viewable; import com.fsck.k9.mailstore.CryptoResultAnnotation.CryptoError; import com.fsck.k9.message.extractors.AttachmentInfoExtractor; import com.fsck.k9.message.html.HtmlConverter; -import com.fsck.k9.message.html.HtmlProcessor; +import app.k9mail.html.cleaner.HtmlProcessor; import org.openintents.openpgp.util.OpenPgpUtils; import timber.log.Timber; diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/SpecialLocalFoldersCreator.kt b/app/core/src/main/java/com/fsck/k9/mailstore/SpecialLocalFoldersCreator.kt index a403ffd7a8b72da481f594c39ab7c2cf259e5e89..c2cd90f605ebbe96b973ae226db28f99b2bc4c5e 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/SpecialLocalFoldersCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/SpecialLocalFoldersCreator.kt @@ -50,6 +50,18 @@ class SpecialLocalFoldersCreator( preferences.saveAccount(account) } + fun createOutbox(account: Account): Long { + Timber.d("Creating Outbox folder") + + val localStore = localStoreProvider.getInstance(account) + val outboxFolderId = localStore.createLocalFolder(OUTBOX_FOLDER_NAME, FolderType.OUTBOX) + + account.outboxFolderId = outboxFolderId + preferences.saveAccount(account) + + return outboxFolderId + } + private fun Account.isPop3() = incomingServerSettings.type == Protocols.POP3 companion object { diff --git a/app/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt b/app/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt index fe56e423e452b5f022add3c8bf6d5594b9839830..9c009648a9b086e3ef299a5943f042f7f8343363 100644 --- a/app/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt +++ b/app/core/src/main/java/com/fsck/k9/message/html/DisplayHtml.kt @@ -1,6 +1,15 @@ package com.fsck.k9.message.html -class DisplayHtml(private val settings: HtmlSettings) { +import app.k9mail.html.cleaner.HtmlHeadProvider + +class DisplayHtml(private val settings: HtmlSettings) : HtmlHeadProvider { + override val headHtml: String + get() { + return """""" + + cssStyleTheme() + + cssStylePre() + + cssStyleSignature() + } fun wrapStatusMessage(status: CharSequence): String { return wrapMessageContent("

$status
") @@ -16,7 +25,7 @@ class DisplayHtml(private val settings: HtmlSettings) { "" } - fun cssStyleTheme(): String { + private fun cssStyleTheme(): String { return if (settings.useDarkMode) { "" } - fun cssStyleSignature(): String { + private fun cssStyleSignature(): String { return """""" } } diff --git a/app/core/src/main/java/com/fsck/k9/message/html/HeadCleaner.java b/app/core/src/main/java/com/fsck/k9/message/html/HeadCleaner.java deleted file mode 100644 index 2ab7ce543ad92ad30248fbf02ef0bdff914eaade..0000000000000000000000000000000000000000 --- a/app/core/src/main/java/com/fsck/k9/message/html/HeadCleaner.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.fsck.k9.message.html; - - -import java.util.List; -import java.util.Locale; - -import org.jsoup.nodes.Attributes; -import org.jsoup.nodes.DataNode; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.nodes.Node; -import org.jsoup.nodes.TextNode; -import org.jsoup.parser.Tag; -import org.jsoup.select.NodeTraversor; -import org.jsoup.select.NodeVisitor; - -import static java.util.Arrays.asList; - - -class HeadCleaner { - private static final List ALLOWED_TAGS = asList("style", "meta"); - - - public void clean(Document dirtyDocument, Document cleanedDocument) { - copySafeNodes(dirtyDocument.head(), cleanedDocument.head()); - } - - private void copySafeNodes(Element source, Element destination) { - CleaningVisitor cleaningVisitor = new CleaningVisitor(source, destination); - NodeTraversor.traverse(cleaningVisitor, source); - } - - - static class CleaningVisitor implements NodeVisitor { - private final Element root; - private Element destination; - private Element elementToSkip; - - - CleaningVisitor(Element root, Element destination) { - this.root = root; - this.destination = destination; - } - - public void head(Node source, int depth) { - if (elementToSkip != null) { - return; - } - - if (source instanceof Element) { - Element sourceElement = (Element) source; - - if (isSafeTag(sourceElement)) { - String sourceTag = sourceElement.tagName(); - Attributes destinationAttributes = sourceElement.attributes().clone(); - Element destinationChild = new Element(Tag.valueOf(sourceTag), sourceElement.baseUri(), destinationAttributes); - - destination.appendChild(destinationChild); - destination = destinationChild; - } else if (source != root) { - elementToSkip = sourceElement; - } - } else if (source instanceof TextNode) { - TextNode sourceText = (TextNode) source; - TextNode destinationText = new TextNode(sourceText.getWholeText()); - destination.appendChild(destinationText); - } else if (source instanceof DataNode && isSafeTag(source.parent())) { - DataNode sourceData = (DataNode) source; - DataNode destinationData = new DataNode(sourceData.getWholeData()); - destination.appendChild(destinationData); - } - } - - public void tail(Node source, int depth) { - if (source == elementToSkip) { - elementToSkip = null; - } else if (source instanceof Element && isSafeTag(source)) { - destination = destination.parent(); - } - } - - private boolean isSafeTag(Node node) { - if (isMetaRefresh(node)) { - return false; - } - - String tag = node.nodeName().toLowerCase(Locale.ROOT); - return ALLOWED_TAGS.contains(tag); - } - - private boolean isMetaRefresh(Node node) { - if (!"meta".equalsIgnoreCase(node.nodeName())) { - return false; - } - - String attributeValue = node.attributes().getIgnoreCase("http-equiv"); - return "refresh".equalsIgnoreCase(attributeValue.trim()); - } - } -} diff --git a/app/core/src/main/java/com/fsck/k9/message/html/HtmlProcessor.java b/app/core/src/main/java/com/fsck/k9/message/html/HtmlProcessor.java deleted file mode 100644 index d8806bde35c1d27be51c273cedff2679ace19f23..0000000000000000000000000000000000000000 --- a/app/core/src/main/java/com/fsck/k9/message/html/HtmlProcessor.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.fsck.k9.message.html; - - -import org.jsoup.nodes.Document; - - -public class HtmlProcessor { - private final HtmlSanitizer htmlSanitizer; - private final DisplayHtml displayHtml; - - - HtmlProcessor(HtmlSanitizer htmlSanitizer, DisplayHtml displayHtml) { - this.htmlSanitizer = htmlSanitizer; - this.displayHtml = displayHtml; - } - - public String processForDisplay(String html) { - Document document = htmlSanitizer.sanitize(html); - addCustomHeadContents(document); - - return toCompactString(document); - } - - private void addCustomHeadContents(Document document) { - document.head().append("" + - displayHtml.cssStyleTheme() + - displayHtml.cssStylePre() + - displayHtml.cssStyleSignature()); - } - - public static String toCompactString(Document document) { - document.outputSettings() - .prettyPrint(false) - .indentAmount(0); - - return document.html(); - } -} diff --git a/app/core/src/main/java/com/fsck/k9/message/html/HtmlProcessorFactory.kt b/app/core/src/main/java/com/fsck/k9/message/html/HtmlProcessorFactory.kt index 3ba32604def08fa5877ecd8289df326efe85cc5c..555bcbe0472285ad9b35a5b2a659bf44a64fbfd4 100644 --- a/app/core/src/main/java/com/fsck/k9/message/html/HtmlProcessorFactory.kt +++ b/app/core/src/main/java/com/fsck/k9/message/html/HtmlProcessorFactory.kt @@ -1,11 +1,12 @@ package com.fsck.k9.message.html +import app.k9mail.html.cleaner.HtmlProcessor + class HtmlProcessorFactory( - private val htmlSanitizer: HtmlSanitizer, private val displayHtmlFactory: DisplayHtmlFactory ) { fun create(settings: HtmlSettings): HtmlProcessor { val displayHtml = displayHtmlFactory.create(settings) - return HtmlProcessor(htmlSanitizer, displayHtml) + return HtmlProcessor(displayHtml) } } diff --git a/app/core/src/main/java/com/fsck/k9/message/html/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/message/html/KoinModule.kt index 662afe6be1b9ffca0cd60919c96023ac8e5f226d..7c6151ad0c85b73f2e117548ca00b85bc66ad235 100644 --- a/app/core/src/main/java/com/fsck/k9/message/html/KoinModule.kt +++ b/app/core/src/main/java/com/fsck/k9/message/html/KoinModule.kt @@ -3,7 +3,6 @@ package com.fsck.k9.message.html import org.koin.dsl.module val htmlModule = module { - single { HtmlProcessorFactory(get(), get()) } - single { HtmlSanitizer() } + single { HtmlProcessorFactory(displayHtmlFactory = get()) } single { DisplayHtmlFactory() } } diff --git a/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.java b/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.java index 86e59ba963939dab6a84be50f86cd29baaee692e..175623b05d2929e102d15971eaf7464daed7e6ba 100644 --- a/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.java +++ b/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.java @@ -4,10 +4,8 @@ package com.fsck.k9.message.signature; import java.util.regex.Pattern; import androidx.annotation.NonNull; - import com.fsck.k9.helper.jsoup.AdvancedNodeTraversor; import com.fsck.k9.helper.jsoup.NodeFilter; -import com.fsck.k9.message.html.HtmlProcessor; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -27,7 +25,15 @@ public class HtmlSignatureRemover { AdvancedNodeTraversor nodeTraversor = new AdvancedNodeTraversor(new StripSignatureFilter()); nodeTraversor.filter(document.body()); - return HtmlProcessor.toCompactString(document); + return toCompactString(document); + } + + private String toCompactString(Document document) { + document.outputSettings() + .prettyPrint(false) + .indentAmount(0); + + return document.html(); } 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 index fad5368c95f9e1e9a72b5a6465592653274fed70..2231a3fd3925c9ec9e6d5a5c5b909ae503e59749 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/AuthenticationErrorNotificationController.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/AuthenticationErrorNotificationController.kt @@ -15,7 +15,7 @@ internal open class AuthenticationErrorNotificationController( val notificationId = NotificationIds.getAuthenticationErrorNotificationId(account, incoming) val editServerSettingsPendingIntent = createContentIntent(account, incoming) val title = resourceProvider.authenticationErrorTitle() - val text = resourceProvider.authenticationErrorBody(account.description) + val text = resourceProvider.authenticationErrorBody(account.displayName) val notificationBuilder = notificationHelper .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS) diff --git a/app/core/src/main/java/com/fsck/k9/notification/BaseNotificationDataCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/BaseNotificationDataCreator.kt index 936f884c4dd10dee9d0a579e8b1d13e3e890412e..b07b78276b323e270c935681741d16e9bdaec600 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/BaseNotificationDataCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/BaseNotificationDataCreator.kt @@ -13,7 +13,7 @@ internal class BaseNotificationDataCreator { return BaseNotificationData( account = account, groupKey = NotificationGroupKeys.getGroupKey(account), - accountName = getAccountName(account), + accountName = account.displayName, color = account.chipColor, newMessagesCount = notificationData.newMessagesCount, lockScreenNotificationData = createLockScreenNotificationData(notificationData), @@ -21,11 +21,6 @@ internal class BaseNotificationDataCreator { ) } - 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 @@ -45,9 +40,9 @@ internal class BaseNotificationDataCreator { } private fun createNotificationAppearance(account: Account): NotificationAppearance { - return with(account.notificationSetting) { - val vibrationPattern = if (isVibrateEnabled) vibration else null - NotificationAppearance(ringtone, vibrationPattern, ledColor) + return with(account.notificationSettings) { + val vibrationPattern = if (isVibrateEnabled) vibrationPattern else null + NotificationAppearance(ringtone, vibrationPattern, account.notificationSettings.light.toColor(account)) } } } 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 index e787b85efbaa09c9ec7da7424dc48438599b138b..ba657a240261aee784a6031ccb9aec89a48e1306 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/CertificateErrorNotificationController.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/CertificateErrorNotificationController.kt @@ -14,7 +14,7 @@ internal open class CertificateErrorNotificationController( fun showCertificateErrorNotification(account: Account, incoming: Boolean) { val notificationId = NotificationIds.getCertificateErrorNotificationId(account, incoming) val editServerSettingsPendingIntent = createContentIntent(account, incoming) - val title = resourceProvider.certificateErrorTitle(account.description) + val title = resourceProvider.certificateErrorTitle(account.displayName) val text = resourceProvider.certificateErrorBody() val notificationBuilder = notificationHelper 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 dba15df01c6a5cf718223dd1e12c7479fe2227bf..970d3515b31efd6cbf95e1f2e47d1325617e743c 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 @@ -24,7 +24,8 @@ val coreNotificationModule = module { preferences = get(), backgroundExecutor = Executors.newSingleThreadExecutor(), notificationManager = get().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager, - resourceProvider = get() + resourceProvider = get(), + notificationLightDecoder = get() ) } single { @@ -112,4 +113,5 @@ val coreNotificationModule = module { notificationContentCreator = get() ) } + factory { NotificationLightDecoder() } } 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 9597b082d5cb3e3d387e6b6d5748fee11838ea5a..55b12338013d16fae44d0092d8704dba826ba4b2 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 @@ -1,12 +1,16 @@ package com.fsck.k9.notification +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationChannelGroup import android.app.NotificationManager +import android.net.Uri import android.os.Build import androidx.annotation.RequiresApi +import androidx.core.net.toUri import com.fsck.k9.Account -import com.fsck.k9.NotificationSetting +import com.fsck.k9.NotificationLight +import com.fsck.k9.NotificationSettings import com.fsck.k9.Preferences import java.util.concurrent.Executor import timber.log.Timber @@ -15,7 +19,8 @@ class NotificationChannelManager( private val preferences: Preferences, private val backgroundExecutor: Executor, private val notificationManager: NotificationManager, - private val resourceProvider: NotificationResourceProvider + private val resourceProvider: NotificationResourceProvider, + private val notificationLightDecoder: NotificationLightDecoder ) { val pushChannelId = "push" @@ -152,6 +157,7 @@ class NotificationChannelManager( val notificationChannel = notificationManager.getNotificationChannel(channelId) return NotificationConfiguration( + sound = notificationChannel.sound, isBlinkLightsEnabled = notificationChannel.shouldShowLights(), lightColor = notificationChannel.lightColor, isVibrationEnabled = notificationChannel.shouldVibrate(), @@ -165,7 +171,7 @@ class NotificationChannelManager( val oldChannelId = getChannelIdFor(account, ChannelType.MESSAGES) val oldNotificationChannel = notificationManager.getNotificationChannel(oldChannelId) - if (oldNotificationChannel.matches(account.notificationSetting)) { + if (oldNotificationChannel.matches(account)) { Timber.v("Not recreating NotificationChannel. The current one already matches the app's settings.") return } @@ -183,17 +189,27 @@ class NotificationChannelManager( group = account.uuid copyPropertiesFrom(oldNotificationChannel) - copyPropertiesFrom(account.notificationSetting) + copyPropertiesFrom(account) } Timber.v("Recreating NotificationChannel(%s => %s)", oldChannelId, newChannelId) + Timber.v("Old NotificationChannel: %s", oldNotificationChannel) + Timber.v("New NotificationChannel: %s", newNotificationChannel) notificationManager.createNotificationChannel(newNotificationChannel) } @RequiresApi(Build.VERSION_CODES.O) - private fun NotificationChannel.matches(notificationSetting: NotificationSetting): Boolean { - return lightColor == notificationSetting.ledColor && - vibrationPattern.contentEquals(notificationSetting.vibration) + private fun NotificationChannel.matches(account: Account): Boolean { + val systemLight = notificationLightDecoder.decode( + isBlinkLightsEnabled = shouldShowLights(), + lightColor = lightColor, + accountColor = account.chipColor + ) + val notificationSettings = account.notificationSettings + return sound == notificationSettings.ringtoneUri && + systemLight == notificationSettings.light && + shouldVibrate() == notificationSettings.isVibrateEnabled && + vibrationPattern.contentEquals(notificationSettings.vibrationPattern) } @RequiresApi(Build.VERSION_CODES.O) @@ -210,18 +226,32 @@ class NotificationChannelManager( } @RequiresApi(Build.VERSION_CODES.O) - private fun NotificationChannel.copyPropertiesFrom(notificationSetting: NotificationSetting) { - lightColor = notificationSetting.ledColor - if (shouldVibrate()) { - vibrationPattern = notificationSetting.vibration + private fun NotificationChannel.copyPropertiesFrom(account: Account) { + val notificationSettings = account.notificationSettings + + if (notificationSettings.isRingEnabled) { + setSound(notificationSettings.ringtone?.toUri(), Notification.AUDIO_ATTRIBUTES_DEFAULT) + } + + notificationSettings.light.toColor(account)?.let { lightColor -> + this.lightColor = lightColor } + val isLightEnabled = notificationSettings.light != NotificationLight.Disabled + enableLights(isLightEnabled) + + vibrationPattern = notificationSettings.vibrationPattern + enableVibration(notificationSettings.isVibrateEnabled) } private val Account.messagesNotificationChannelSuffix: String get() = messagesNotificationChannelVersion.let { version -> if (version == 0) "" else "_$version" } + + private val NotificationSettings.ringtoneUri: Uri? + get() = if (isRingEnabled) ringtone?.toUri() else null } data class NotificationConfiguration( + val sound: Uri?, val isBlinkLightsEnabled: Boolean, val lightColor: Int, val isVibrationEnabled: Boolean, diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt index 51372b0a939d6c83d1815b49d19aff957ce0e443..50cd1be99e1d0301032b7cca8a0f4f803f4c71a2 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt @@ -2,6 +2,7 @@ package com.fsck.k9.notification import com.fsck.k9.Account import com.fsck.k9.controller.MessageReference +import com.fsck.k9.core.BuildConfig internal const val MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS = 8 @@ -31,6 +32,11 @@ internal class NotificationDataStore { @Synchronized fun addNotification(account: Account, content: NotificationContent, timestamp: Long): AddNotificationResult { val notificationData = getNotificationData(account) + val messageReference = content.messageReference + + if (BuildConfig.DEBUG && notificationData.contains(messageReference)) { + throw AssertionError("Notification for message $messageReference already exists") + } return if (notificationData.isMaxNumberOfActiveNotificationsReached) { val lastNotificationHolder = notificationData.activeNotifications.last() @@ -41,7 +47,7 @@ internal class NotificationDataStore { val operations = listOf( NotificationStoreOperation.ChangeToInactive(lastNotificationHolder.content.messageReference), - NotificationStoreOperation.Add(content.messageReference, notificationId, timestamp) + NotificationStoreOperation.Add(messageReference, notificationId, timestamp) ) val newNotificationData = notificationData.copy( @@ -56,7 +62,7 @@ internal class NotificationDataStore { val notificationHolder = NotificationHolder(notificationId, timestamp, content) val operations = listOf( - NotificationStoreOperation.Add(content.messageReference, notificationId, timestamp) + NotificationStoreOperation.Add(messageReference, notificationId, timestamp) ) val newNotificationData = notificationData.copy( @@ -169,6 +175,11 @@ internal class NotificationDataStore { throw AssertionError("getNewNotificationId() called with no free notification ID") } + private fun NotificationData.contains(messageReference: MessageReference): Boolean { + return activeNotifications.any { it.content.messageReference == messageReference } || + inactiveNotifications.any { it.content.messageReference == messageReference } + } + private fun NotificationHolder.toInactiveNotificationHolder() = InactiveNotificationHolder(timestamp, content) private fun InactiveNotificationHolder.toNotificationHolder(notificationId: Int): NotificationHolder { 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 9199fa7ad08dd15ac8700c9c2ff05639d55084b4..4b85b884cb47ef30df3a93f73ddacddbf508bde8 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 @@ -54,11 +54,6 @@ class NotificationHelper( } } - fun getAccountName(account: Account): String { - val accountDescription = account.description - return if (TextUtils.isEmpty(accountDescription)) account.email else accountDescription - } - fun getContext(): Context { return context } diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationLightDecoder.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationLightDecoder.kt new file mode 100644 index 0000000000000000000000000000000000000000..b849f14f916c7c2cb4d1380f5680d9d4144e9e6c --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationLightDecoder.kt @@ -0,0 +1,27 @@ +package com.fsck.k9.notification + +import com.fsck.k9.NotificationLight + +/** + * Converts the "blink lights" values read from a `NotificationChannel` into [NotificationLight]. + */ +class NotificationLightDecoder { + fun decode(isBlinkLightsEnabled: Boolean, lightColor: Int, accountColor: Int): NotificationLight { + if (!isBlinkLightsEnabled) return NotificationLight.Disabled + + return when (lightColor.rgb) { + accountColor.rgb -> NotificationLight.AccountColor + 0xFFFFFF -> NotificationLight.White + 0xFF0000 -> NotificationLight.Red + 0x00FF00 -> NotificationLight.Green + 0x0000FF -> NotificationLight.Blue + 0xFFFF00 -> NotificationLight.Yellow + 0x00FFFF -> NotificationLight.Cyan + 0xFF00FF -> NotificationLight.Magenta + else -> NotificationLight.SystemDefaultColor + } + } + + private val Int.rgb + get() = this and 0x00FFFFFF +} diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationRepository.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationRepository.kt index b18f3686aa7259b3179e3482ad0d55fdff644f86..f624810c990bb45730f96bbe4d294d6c480f973e 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/NotificationRepository.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationRepository.kt @@ -39,7 +39,11 @@ internal class NotificationRepository( @Synchronized fun addNotification(account: Account, content: NotificationContent, timestamp: Long): AddNotificationResult { return notificationDataStore.addNotification(account, content, timestamp).also { result -> - persistNotificationDataStoreChanges(account, result.notificationStoreOperations) + persistNotificationDataStoreChanges( + account = account, + operations = result.notificationStoreOperations, + updateNewMessageState = true + ) } } @@ -50,9 +54,11 @@ internal class NotificationRepository( selector: (List) -> List ): RemoveNotificationsResult? { return notificationDataStore.removeNotifications(account, selector)?.also { result -> - if (clearNewMessageState) { - persistNotificationDataStoreChanges(account, result.notificationStoreOperations) - } + persistNotificationDataStoreChanges( + account = account, + operations = result.notificationStoreOperations, + updateNewMessageState = clearNewMessageState + ) } } @@ -66,11 +72,17 @@ internal class NotificationRepository( } } - private fun persistNotificationDataStoreChanges(account: Account, operations: List) { + private fun persistNotificationDataStoreChanges( + account: Account, + operations: List, + updateNewMessageState: Boolean + ) { val notificationStore = notificationStoreProvider.getNotificationStore(account) notificationStore.persistNotificationChanges(operations) - setNewMessageState(account, operations) + if (updateNewMessageState) { + setNewMessageState(account, operations) + } } private fun setNewMessageState(account: Account, operations: List) { 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 79b25a37356ae0d75f5a20b77921a54b812810bd..fbce51ac4f21f685b380f9e23800a1f8d31cf668 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 @@ -4,10 +4,10 @@ import android.app.Notification import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.fsck.k9.CoreResourceProvider +import com.fsck.k9.helper.PendingIntentCompat.FLAG_IMMUTABLE private const val PUSH_INFO_ACTION = "app.k9mail.action.PUSH_INFO" @@ -56,8 +56,7 @@ internal class PushNotificationManager( addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) setPackage(context.packageName) } - val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 - val contentIntent = PendingIntent.getActivity(context, 1, intent, flag) + val contentIntent = PendingIntent.getActivity(context, 1, intent, FLAG_IMMUTABLE) return NotificationCompat.Builder(context, notificationChannelManager.pushChannelId) .setSmallIcon(resourceProvider.iconPushNotification) diff --git a/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt index 067893a8e2ad1193b3864e6b76e33d892117224f..a75b73f5af942343e049a948464ae3be4320048a 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt @@ -16,9 +16,18 @@ internal class SendFailedNotificationController( val text = ExceptionHelper.getRootCauseMessage(exception) val notificationId = NotificationIds.getSendFailedNotificationId(account) - val folderListPendingIntent = actionBuilder.createViewFolderListPendingIntent( - account, notificationId - ) + + val pendingIntent = account.outboxFolderId.let { outboxFolderId -> + if (outboxFolderId != null) { + actionBuilder.createViewFolderPendingIntent( + account, outboxFolderId, notificationId + ) + } else { + actionBuilder.createViewFolderListPendingIntent( + account, notificationId + ) + } + } val notificationBuilder = notificationHelper .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS) @@ -29,7 +38,7 @@ internal class SendFailedNotificationController( .setTicker(title) .setContentTitle(title) .setContentText(text) - .setContentIntent(folderListPendingIntent) + .setContentIntent(pendingIntent) .setStyle(NotificationCompat.BigTextStyle().bigText(text)) .setPublicVersion(createLockScreenNotification(account)) .setCategory(NotificationCompat.CATEGORY_ERROR) diff --git a/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt index 46635307a418863fb74749631861464da3d8384b..45027ec8206f8af71f42647ab547c5967faa9f34 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt @@ -5,6 +5,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.WearableExtender import androidx.core.app.NotificationManagerCompat import com.fsck.k9.notification.NotificationChannelManager.ChannelType +import timber.log.Timber import androidx.core.app.NotificationCompat.Builder as NotificationBuilder internal class SingleMessageNotificationCreator( @@ -44,6 +45,13 @@ internal class SingleMessageNotificationCreator( .setLockScreenNotification(baseNotificationData, singleNotificationData.addLockScreenNotification) .build() + if (isGroupSummary) { + Timber.v( + "Creating single summary notification (silent=%b): %s", + singleNotificationData.isSilent, + notification + ) + } notificationManager.notify(notificationId, notification) } diff --git a/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt index d6b85a3aef2bb2e41718d2e71d16fad9fbf7a5cb..bcd4f72b3f9da82e2f40f115b7538ee43a2e1948 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt @@ -7,6 +7,7 @@ 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 timber.log.Timber import androidx.core.app.NotificationCompat.Builder as NotificationBuilder internal class SummaryNotificationCreator( @@ -73,6 +74,7 @@ internal class SummaryNotificationCreator( .setLockScreenNotification(baseNotificationData) .build() + Timber.v("Creating inbox-style summary notification (silent=%b): %s", notificationData.isSilent, notification) notificationManager.notify(notificationData.notificationId, notification) } diff --git a/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt index 4d4390ef5294504c16beb9925450338d707a4437..fa318ffa976575190a2f654c785a9d2273e73b94 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt @@ -14,7 +14,7 @@ internal class SyncNotificationController( private val resourceProvider: NotificationResourceProvider ) { fun showSendingNotification(account: Account) { - val accountName = notificationHelper.getAccountName(account) + val accountName = account.displayName val title = resourceProvider.sendingMailTitle() val tickerText = resourceProvider.sendingMailBody(accountName) @@ -41,7 +41,7 @@ internal class SyncNotificationController( builder = notificationBuilder, ringtone = null, vibrationPattern = null, - ledColor = account.notificationSetting.ledColor, + ledColor = account.notificationSettings.light.toColor(account), ledSpeed = NotificationHelper.NOTIFICATION_LED_BLINK_FAST, ringAndVibrate = true ) @@ -56,7 +56,7 @@ internal class SyncNotificationController( } fun showFetchingMailNotification(account: Account, folder: LocalFolder) { - val accountName = account.description + val accountName = account.displayName val folderId = folder.databaseId val folderName = folder.name val tickerText = resourceProvider.checkingMailTicker(accountName, folderName) @@ -88,7 +88,7 @@ internal class SyncNotificationController( builder = notificationBuilder, ringtone = null, vibrationPattern = null, - ledColor = account.notificationSetting.ledColor, + ledColor = account.notificationSettings.light.toColor(account), ledSpeed = NotificationHelper.NOTIFICATION_LED_BLINK_FAST, ringAndVibrate = true ) @@ -99,7 +99,7 @@ internal class SyncNotificationController( fun showEmptyFetchingMailNotification(account: Account) { val title = resourceProvider.checkingMailTitle() - val text = account.description + val text = account.displayName val notificationId = NotificationIds.getFetchingMailNotificationId(account) val notificationBuilder = notificationHelper @@ -118,7 +118,7 @@ internal class SyncNotificationController( builder = notificationBuilder, ringtone = null, vibrationPattern = null, - ledColor = account.notificationSetting.ledColor, + ledColor = account.notificationSettings.light.toColor(account), ledSpeed = NotificationHelper.NOTIFICATION_LED_BLINK_FAST, ringAndVibrate = true ) 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 04f2df62ce2682017cff0121b9367c220e7057e0..bb939fe095768711d7f0aa7ad29ce8b82566bc95 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 @@ -23,8 +23,10 @@ import com.fsck.k9.Account.SpecialFolderSelection; import com.fsck.k9.AccountPreferenceSerializer; import com.fsck.k9.DI; import com.fsck.k9.K9; +import com.fsck.k9.NotificationLight; import com.fsck.k9.core.R; import com.fsck.k9.mailstore.StorageManager; +import com.fsck.k9.notification.NotificationLightDecoder; import com.fsck.k9.preferences.Settings.BooleanSetting; import com.fsck.k9.preferences.Settings.ColorSetting; import com.fsck.k9.preferences.Settings.EnumSetting; @@ -35,6 +37,7 @@ import com.fsck.k9.preferences.Settings.SettingsDescription; import com.fsck.k9.preferences.Settings.SettingsUpgrader; import com.fsck.k9.preferences.Settings.StringSetting; import com.fsck.k9.preferences.Settings.V; +import kotlin.collections.SetsKt; public class AccountSettingsDescriptions { @@ -106,10 +109,12 @@ public class AccountSettingsDescriptions { new V(74, new IntegerResourceSetting(24, R.array.idle_refresh_period_values)) )); s.put("led", Settings.versions( - new V(1, new BooleanSetting(true)) + new V(1, new BooleanSetting(true)), + new V(80, null) )); s.put("ledColor", Settings.versions( - new V(1, new ColorSetting(0xFF0000FF)) + new V(1, new ColorSetting(0xFF0000FF)), + new V(80, null) )); s.put("localStorageProvider", Settings.versions( new V(1, new StorageProviderSetting()) @@ -219,9 +224,6 @@ public class AccountSettingsDescriptions { s.put("vibrateTimes", Settings.versions( new V(1, new IntegerRangeSetting(1, 10, 5)) )); - s.put("allowRemoteSearch", Settings.versions( - new V(18, new BooleanSetting(true)) - )); s.put("remoteSearchNumResults", Settings.versions( new V(18, new IntegerResourceSetting(AccountPreferenceSerializer.DEFAULT_REMOTE_SEARCH_NUM_RESULTS, R.array.remote_search_num_results_values)) @@ -265,6 +267,9 @@ public class AccountSettingsDescriptions { s.put("ignoreChatMessages", Settings.versions( new V(76, new BooleanSetting(false)) )); + s.put("notificationLight", Settings.versions( + new V(80, new EnumSetting<>(NotificationLight.class, NotificationLight.Disabled)) + )); // note that there is no setting for openPgpProvider, because this will have to be set up together // with the actual provider after import anyways. @@ -274,6 +279,7 @@ public class AccountSettingsDescriptions { u.put(53, new SettingsUpgraderV53()); u.put(54, new SettingsUpgraderV54()); u.put(74, new SettingsUpgraderV74()); + u.put(80, new SettingsUpgraderV80()); UPGRADERS = Collections.unmodifiableMap(u); } @@ -516,4 +522,28 @@ public class AccountSettingsDescriptions { return null; } } + + /** + * Upgrades settings from version 79 to 80 + * + * Rewrites 'led' and 'lecColor' to 'notificationLight'. + */ + private static class SettingsUpgraderV80 implements SettingsUpgrader { + private final NotificationLightDecoder notificationLightDecoder = DI.get(NotificationLightDecoder.class); + + @Override + public Set upgrade(Map settings) { + Boolean isLedEnabled = (Boolean) settings.get("led"); + Integer ledColor = (Integer) settings.get("ledColor"); + Integer chipColor = (Integer) settings.get("chipColor"); + + if (isLedEnabled != null && ledColor != null) { + int accountColor = chipColor != null ? chipColor : 0; + NotificationLight light = notificationLightDecoder.decode(isLedEnabled, ledColor, accountColor); + settings.put("notificationLight", light.name()); + } + + return SetsKt.setOf("led", "ledColor"); + } + } } 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 7131e174160c804dd9e185d60601cdf15a0ba2c2..34efb299a6663de90f21533e706860e2c7ba3407 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 @@ -10,6 +10,7 @@ import java.util.Set; import java.util.TreeMap; import android.content.Context; +import android.graphics.Color; import com.fsck.k9.Account; import com.fsck.k9.Account.SortType; @@ -157,7 +158,8 @@ public class GeneralSettingsDescriptions { new V(1, new TimeSetting("21:00")) )); s.put("registeredNameColor", Settings.versions( - new V(1, new ColorSetting(0xFF00008F)) + new V(1, new ColorSetting(0xFF00008F)), + new V(79, new ColorSetting(0xFF1093F5)) )); s.put("showContactName", Settings.versions( new V(1, new BooleanSetting(false)) @@ -284,6 +286,7 @@ public class GeneralSettingsDescriptions { u.put(31, new SettingsUpgraderV31()); u.put(58, new SettingsUpgraderV58()); u.put(69, new SettingsUpgraderV69()); + u.put(79, new SettingsUpgraderV79()); UPGRADERS = Collections.unmodifiableMap(u); } @@ -416,6 +419,27 @@ public class GeneralSettingsDescriptions { } } + /** + * Upgrades the settings from version 78 to 79. + * + *

+ * Change default value of {@code registeredNameColor} to have enough contrast in both the light and dark theme. + *

+ */ + private static class SettingsUpgraderV79 implements SettingsUpgrader { + + @Override + public Set upgrade(Map settings) { + final Integer registeredNameColorValue = (Integer) settings.get("registeredNameColor"); + + if (registeredNameColorValue != null && registeredNameColorValue == 0xFF00008F) { + settings.put("registeredNameColor", 0xFF1093F5); + } + + return null; + } + } + private static class LanguageSetting extends PseudoEnumSetting { private final Context context = DI.get(Context.class); private final Map mapping; 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 67205834947cf687b63ce97055e3ca4a89f81309..8124a0c345ebdad258bd3cd9909bf13af8f892fc 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 = 78; + public static final int VERSION = 80; 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 0e879125dfbd519aeace8aa0016d4f8e1b08b963..7362d9d7312acf88b30797575f2b084ff2ac641f 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 @@ -257,7 +257,7 @@ class SettingsExporter( Timber.w( "Account setting \"%s\" (%s) has invalid value \"%s\" in preference storage. " + "This shouldn't happen!", - keyPart, account.description, valueString + keyPart, account, valueString ) } } 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 e07208a1679e276680daff214da8ebd56df3b7c7..ba83f5a8c93f256104f126622a05354478e42e4e 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 @@ -596,7 +596,7 @@ public class SettingsImporter { continue; } - if (account.getDescription().equals(name)) { + if (account.getDisplayName().equals(name)) { return true; } } diff --git a/app/core/src/main/java/com/fsck/k9/preferences/Storage.java b/app/core/src/main/java/com/fsck/k9/preferences/Storage.java index 56baaff19983aff83b9f6b4a9dad7eda569873e5..a515d5825cf466ca989ffc23b324a11cba4555dc 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/Storage.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/Storage.java @@ -2,30 +2,31 @@ package com.fsck.k9.preferences; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import timber.log.Timber; public class Storage { - private volatile Map storage = Collections.emptyMap(); + private final Map values; - public Storage() { } + public Storage(Map values) { + this.values = Collections.unmodifiableMap(values); + } public boolean isEmpty() { - return storage.isEmpty(); + return values.isEmpty(); } public boolean contains(String key) { - return storage.containsKey(key); + return values.containsKey(key); } public Map getAll() { - return storage; + return values; } public boolean getBoolean(String key, boolean defValue) { - String val = storage.get(key); + String val = values.get(key); if (val == null) { return defValue; } @@ -33,7 +34,7 @@ public class Storage { } public int getInt(String key, int defValue) { - String val = storage.get(key); + String val = values.get(key); if (val == null) { return defValue; } @@ -46,7 +47,7 @@ public class Storage { } public long getLong(String key, long defValue) { - String val = storage.get(key); + String val = values.get(key); if (val == null) { return defValue; } @@ -59,14 +60,10 @@ public class Storage { } public String getString(String key, String defValue) { - String val = storage.get(key); + String val = values.get(key); if (val == null) { return defValue; } return val; } - - public void replaceAll(Map workingStorage) { - storage = new HashMap<>(workingStorage); - } } diff --git a/app/core/src/main/java/com/fsck/k9/preferences/StoragePersister.kt b/app/core/src/main/java/com/fsck/k9/preferences/StoragePersister.kt index 9a01e28fe7eee0a39cc3a992b92f044ffb3c6de9..5f14f6d4c3ab6e45ed3f1c12ce5346c231cc60c0 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/StoragePersister.kt +++ b/app/core/src/main/java/com/fsck/k9/preferences/StoragePersister.kt @@ -1,10 +1,11 @@ package com.fsck.k9.preferences -import androidx.annotation.CheckResult - interface StoragePersister { - @CheckResult - fun loadValues(): Map + fun loadValues(): Storage + + fun createStorageEditor(storageUpdater: StorageUpdater): StorageEditor +} - fun createStorageEditor(storage: Storage): StorageEditor +fun interface StorageUpdater { + fun updateStorage(updater: (currentStorage: Storage) -> Storage) } 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 deleted file mode 100644 index b5ccbacb30d126529af5fed8a8546944a412af1c..0000000000000000000000000000000000000000 --- a/app/core/src/main/java/com/fsck/k9/search/SearchAccount.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.fsck.k9.search; - - -import com.fsck.k9.BaseAccount; -import com.fsck.k9.CoreResourceProvider; -import com.fsck.k9.DI; -import com.fsck.k9.search.SearchSpecification.Attribute; -import com.fsck.k9.search.SearchSpecification.SearchField; - - -/** - * This class is basically a wrapper around a LocalSearch. It allows to expose it as - * an account. This is a meta-account containing all the email that matches the search. - */ -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 ) - public static SearchAccount createUnifiedInboxAccount() { - CoreResourceProvider resourceProvider = DI.get(CoreResourceProvider.class); - LocalSearch tmpSearch = new LocalSearch(); - tmpSearch.setId(UNIFIED_INBOX); - tmpSearch.and(SearchField.INTEGRATE, "1", Attribute.EQUALS); - return new SearchAccount(UNIFIED_INBOX, tmpSearch, resourceProvider.searchUnifiedInboxTitle(), - resourceProvider.searchUnifiedInboxDetail()); - } - - private String mId; - private String mEmail; - private String mDescription; - private LocalSearch mSearch; - - public SearchAccount(String id, LocalSearch search, String description, String email) - throws IllegalArgumentException { - - if (search == null) { - throw new IllegalArgumentException("Provided LocalSearch was null"); - } - - mId = id; - mSearch = search; - mDescription = description; - mEmail = email; - } - - public String getId() { - return mId; - } - - @Override - public synchronized String getEmail() { - return mEmail; - } - - public synchronized void setEmail(String email) { - this.mEmail = email; - } - - @Override - public String getDescription() { - return mDescription; - } - - public void setDescription(String description) { - this.mDescription = description; - } - - public LocalSearch getRelatedSearch() { - return mSearch; - } - - /** - * Returns the ID of this {@code SearchAccount} instance. - * - *

- * This isn't really a UUID. But since we don't expose this value to other apps and we only - * use the account UUID as opaque string (e.g. as key in a {@code Map}) we're fine.
- * Using a constant string is necessary to identify the same search account even when the - * corresponding {@link SearchAccount} object has been recreated. - *

- */ - @Override - public String getUuid() { - return mId; - } -} diff --git a/app/core/src/main/java/com/fsck/k9/search/SearchAccount.kt b/app/core/src/main/java/com/fsck/k9/search/SearchAccount.kt new file mode 100644 index 0000000000000000000000000000000000000000..68a7abea6f43de5b7171f8b8f7d1887cbd6ef1c4 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/search/SearchAccount.kt @@ -0,0 +1,53 @@ +package com.fsck.k9.search + +import com.fsck.k9.BaseAccount +import com.fsck.k9.CoreResourceProvider +import com.fsck.k9.search.SearchSpecification.SearchField +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * This class is basically a wrapper around a LocalSearch. It allows to expose it as an account. + * This is a meta-account containing all the messages that match the search. + */ +class SearchAccount( + val id: String, + search: LocalSearch, + override val name: String, + override val email: String +) : BaseAccount { + /** + * Returns the ID of this `SearchAccount` instance. + * + * This isn't really a UUID. But since we don't expose this value to other apps and we only use the account UUID + * as opaque string (e.g. as key in a `Map`) we're fine. + * + * Using a constant string is necessary to identify the same search account even when the corresponding + * [SearchAccount] object has been recreated. + */ + override val uuid: String = id + + val relatedSearch: LocalSearch = search + + companion object : KoinComponent { + private val resourceProvider: CoreResourceProvider by inject() + + const val UNIFIED_INBOX = "unified_inbox" + const val NEW_MESSAGES = "new_messages" + + @JvmStatic + fun createUnifiedInboxAccount(): SearchAccount { + val tmpSearch = LocalSearch().apply { + id = UNIFIED_INBOX + and(SearchField.INTEGRATE, "1", SearchSpecification.Attribute.EQUALS) + } + + return SearchAccount( + id = UNIFIED_INBOX, + search = tmpSearch, + name = resourceProvider.searchUnifiedInboxTitle(), + email = resourceProvider.searchUnifiedInboxDetail() + ) + } + } +} 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 42abd1cad48b2eac1a44cb650bd407667a9b8bd1..1946f623a03e1fff005bc7aa5db6aac2c45658d7 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,42 +24,6 @@ 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 5 @@ -213,6 +177,20 @@ 5 + + + Disabled + AccountColor + SystemDefaultColor + White + Red + Green + Blue + Yellow + Cyan + Magenta + + PREFIX HEADER 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 6525e7423d759ec5bd47e6a493175dee14be2797..6fad0313b17fe185e8cf8da05f7f0f52c366047a 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 @@ -31,6 +31,7 @@ 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.SpecialLocalFoldersCreator; import com.fsck.k9.notification.NotificationController; import com.fsck.k9.notification.NotificationStrategy; import com.fsck.k9.preferences.Protocols; @@ -84,6 +85,8 @@ public class MessagingControllerTest extends K9RobolectricTest { @Mock private SaveMessageDataCreator saveMessageDataCreator; @Mock + private SpecialLocalFoldersCreator specialLocalFoldersCreator; + @Mock private SimpleMessagingListener listener; @Mock private LocalSearch search; @@ -139,7 +142,7 @@ public class MessagingControllerTest extends K9RobolectricTest { controller = new MessagingController(appContext, notificationController, notificationStrategy, localStoreProvider, messageCountsProvider, backendManager, preferences, messageStoreManager, - saveMessageDataCreator, Collections.emptyList()); + saveMessageDataCreator, specialLocalFoldersCreator, Collections.emptyList()); configureAccount(); configureBackendManager(); diff --git a/app/core/src/test/java/com/fsck/k9/helper/IdentityHelperTest.kt b/app/core/src/test/java/com/fsck/k9/helper/IdentityHelperTest.kt index 7bae4690d91e2e3984dcaf80821ee8011a8b2ac6..cb2867043ca218ef9973a17fd75de2b763c194e1 100644 --- a/app/core/src/test/java/com/fsck/k9/helper/IdentityHelperTest.kt +++ b/app/core/src/test/java/com/fsck/k9/helper/IdentityHelperTest.kt @@ -115,13 +115,15 @@ class IdentityHelperTest : RobolectricTest() { } private fun createDummyAccount() = Account(UUID.randomUUID().toString()).apply { - identities = listOf( - newIdentity("Default", DEFAULT_ADDRESS), - newIdentity("Identity 1", IDENTITY_1_ADDRESS), - newIdentity("Identity 2", IDENTITY_2_ADDRESS), - newIdentity("Identity 3", IDENTITY_3_ADDRESS), - newIdentity("Identity 4", IDENTITY_4_ADDRESS), - newIdentity("Identity 5", IDENTITY_5_ADDRESS) + replaceIdentities( + listOf( + newIdentity("Default", DEFAULT_ADDRESS), + newIdentity("Identity 1", IDENTITY_1_ADDRESS), + newIdentity("Identity 2", IDENTITY_2_ADDRESS), + newIdentity("Identity 3", IDENTITY_3_ADDRESS), + newIdentity("Identity 4", IDENTITY_4_ADDRESS), + newIdentity("Identity 5", IDENTITY_5_ADDRESS) + ) ) } 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 563286c8e552601eee6ffb27e46cb7c93c6f08cd..65a65de70dc1bba7181ed229f0441dc1b1f08711 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 @@ -35,7 +35,7 @@ import com.fsck.k9.mail.internet.Viewable.MessageHeader; import com.fsck.k9.mailstore.CryptoResultAnnotation.CryptoError; import com.fsck.k9.mailstore.MessageViewInfoExtractor.ViewableExtractedText; import com.fsck.k9.message.extractors.AttachmentInfoExtractor; -import com.fsck.k9.message.html.HtmlProcessor; +import app.k9mail.html.cleaner.HtmlProcessor; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; 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 index af796ff184e9a011110b80a515c898c94e920a2f..aed171f238db3b3e97ab6e0befa57dd9c6c9a96b 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/AuthenticationErrorNotificationControllerTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/AuthenticationErrorNotificationControllerTest.kt @@ -107,7 +107,7 @@ class AuthenticationErrorNotificationControllerTest : RobolectricTest() { private fun createFakeAccount(): Account { return mock { on { accountNumber } doReturn ACCOUNT_NUMBER - on { description } doReturn ACCOUNT_NAME + on { displayName } doReturn ACCOUNT_NAME } } diff --git a/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationDataCreatorTest.kt b/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationDataCreatorTest.kt index 5b0cf007a33b8f46d9b08b749aff18f4bef83565..f1520fc4970a8357af51e7cf2328c7e39aaa0002 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationDataCreatorTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationDataCreatorTest.kt @@ -4,7 +4,9 @@ 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.fsck.k9.NotificationLight +import com.fsck.k9.NotificationSettings +import com.fsck.k9.VibratePattern import com.google.common.truth.Truth.assertThat import org.junit.Test import org.mockito.kotlin.mock @@ -23,19 +25,19 @@ class BaseNotificationDataCreatorTest { } @Test - fun `account name from description property`() { - account.description = "description" + fun `account name from name property`() { + account.name = "name" account.email = "irrelevant@k9mail.example" val notificationData = createNotificationData() val result = notificationDataCreator.createBaseNotificationData(notificationData) - assertThat(result.accountName).isEqualTo("description") + assertThat(result.accountName).isEqualTo("name") } @Test - fun `account description is blank`() { - account.description = "" + fun `account name is blank`() { + account.name = "" account.email = "test@k9mail.example" val notificationData = createNotificationData() @@ -45,8 +47,8 @@ class BaseNotificationDataCreatorTest { } @Test - fun `account description is null`() { - account.description = null + fun `account name is null`() { + account.name = null account.email = "test@k9mail.example" val notificationData = createNotificationData() @@ -138,7 +140,7 @@ class BaseNotificationDataCreatorTest { @Test fun ringtone() { - account.notificationSetting.ringtone = "content://ringtone/1" + account.updateNotificationSettings { it.copy(ringtone = "content://ringtone/1") } val notificationData = createNotificationData() val result = notificationDataCreator.createBaseNotificationData(notificationData) @@ -148,24 +150,26 @@ class BaseNotificationDataCreatorTest { @Test fun `vibration pattern`() { - account.notificationSetting.isVibrateEnabled = true - account.notificationSetting.vibratePattern = 3 - account.notificationSetting.vibrateTimes = 2 + account.updateNotificationSettings { + it.copy(isVibrateEnabled = true, vibratePattern = VibratePattern.Pattern3, vibrateTimes = 2) + } val notificationData = createNotificationData() val result = notificationDataCreator.createBaseNotificationData(notificationData) - assertThat(result.appearance.vibrationPattern).isEqualTo(NotificationSetting.getVibration(3, 2)) + assertThat(result.appearance.vibrationPattern).isEqualTo( + NotificationSettings.getVibrationPattern(VibratePattern.Pattern3, 2) + ) } @Test fun `led color`() { - account.notificationSetting.ledColor = 0x00FF00 + account.updateNotificationSettings { it.copy(light = NotificationLight.Green) } val notificationData = createNotificationData() val result = notificationDataCreator.createBaseNotificationData(notificationData) - assertThat(result.appearance.ledColor).isEqualTo(0x00FF00) + assertThat(result.appearance.ledColor).isEqualTo(0xFF00FF00L.toInt()) } private fun setLockScreenMode(mode: LockScreenNotificationVisibility) { @@ -191,8 +195,8 @@ class BaseNotificationDataCreatorTest { private fun createAccount(): Account { return Account("00000000-0000-4000-0000-000000000000").apply { - description = "account name" - identities = listOf(Identity()) + name = "account name" + replaceIdentities(listOf(Identity())) } } } 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 index f853d1fa68edffd5978cf8e4f8e04209ce79ea7d..74c18c892da832f6448e8bcc02f91efa860d4a44 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/CertificateErrorNotificationControllerTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/CertificateErrorNotificationControllerTest.kt @@ -107,7 +107,7 @@ class CertificateErrorNotificationControllerTest : RobolectricTest() { private fun createFakeAccount(): Account { return mock { on { accountNumber } doReturn ACCOUNT_NUMBER - on { description } doReturn ACCOUNT_NAME + on { displayName } doReturn ACCOUNT_NAME on { uuid } doReturn "test-uuid" } } diff --git a/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt index 873dd7cb5f7f4f683e4ea070b14aabd7526e5d49..72e8a193d582b6c9ecb6cdc08c845efaea478352 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt @@ -347,7 +347,7 @@ class NewMailNotificationManagerTest { private fun createAccount(): Account { return Account(ACCOUNT_UUID).apply { - description = ACCOUNT_NAME + name = ACCOUNT_NAME chipColor = ACCOUNT_COLOR } } diff --git a/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt b/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt index 9bb2c110bb2fd919c5008145a5014d6d75668fb9..a07463909649277ee11aa582f9191a90e1259d64 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt @@ -3,8 +3,12 @@ package com.fsck.k9.notification import com.fsck.k9.Account import com.fsck.k9.RobolectricTest import com.fsck.k9.controller.MessageReference +import com.fsck.k9.core.BuildConfig import com.google.common.truth.Truth.assertThat import kotlin.test.assertNotNull +import org.junit.Assert.fail +import org.junit.Assume.assumeFalse +import org.junit.Assume.assumeTrue import org.junit.Test private const val ACCOUNT_UUID = "1-2-3" @@ -127,6 +131,36 @@ class NotificationDataStoreTest : RobolectricTest() { assertThat(addResult.notificationData.activeNotifications.first()).isEqualTo(addResult.notificationHolder) } + @Test + fun `adding notification for same message twice should throw in debug build`() { + assumeTrue(BuildConfig.DEBUG) + + val content1 = createNotificationContent("1") + val content2 = createNotificationContent("1") + notificationDataStore.addNotification(account, content1, TIMESTAMP) + + try { + notificationDataStore.addNotification(account, content2, TIMESTAMP) + fail("Exception expected") + } catch (e: AssertionError) { + assertThat(e).hasMessageThat().matches("Notification for message .+ already exists") + } + } + + @Test + fun `adding notification for same message twice should add another notification in release build`() { + assumeFalse(BuildConfig.DEBUG) + + val content1 = createNotificationContent("1") + val content2 = createNotificationContent("1") + + val addResult1 = notificationDataStore.addNotification(account, content1, TIMESTAMP) + val addResult2 = notificationDataStore.addNotification(account, content2, TIMESTAMP) + + assertThat(addResult1.notificationHolder.notificationId) + .isNotEqualTo(addResult2.notificationHolder.notificationId) + } + private fun createAccount(): Account { return Account("00000000-0000-4000-0000-000000000000").apply { accountNumber = ACCOUNT_NUMBER diff --git a/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt index 707670d110298796bccf52f0b78a224a1ebed891..591fe1a17609fb539594b5575209dac2267d0655 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt @@ -10,6 +10,7 @@ import com.fsck.k9.RobolectricTest import com.fsck.k9.testing.MockHelper.mockBuilder import org.junit.Test import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.doReturn @@ -81,13 +82,14 @@ class SendFailedNotificationControllerTest : RobolectricTest() { private fun createFakeAccount(): Account { return mock { on { accountNumber } doReturn ACCOUNT_NUMBER - on { description } doReturn ACCOUNT_NAME + on { name } doReturn ACCOUNT_NAME } } private fun createActionBuilder(contentIntent: PendingIntent): NotificationActionCreator { return mock { on { createViewFolderListPendingIntent(any(), anyInt()) } doReturn contentIntent + on { createViewFolderPendingIntent(any(), anyLong(), anyInt()) } doReturn contentIntent } } } diff --git a/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt index 417643a1f05a2a094ce376ea0a3de166e456b60b..cde497225965a636e0bf2b691e01e4bc8d56acf2 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt @@ -126,14 +126,14 @@ class SyncNotificationControllerTest : RobolectricTest() { 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 { name } doReturn ACCOUNT_NAME + on { displayName } doReturn ACCOUNT_NAME on { outboxFolderId } doReturn 33L } } diff --git a/app/core/src/test/java/com/fsck/k9/preferences/StorageTest.kt b/app/core/src/test/java/com/fsck/k9/preferences/StorageTest.kt deleted file mode 100644 index 2bd3e676b514d954fe917db72a38e303a2e96adb..0000000000000000000000000000000000000000 --- a/app/core/src/test/java/com/fsck/k9/preferences/StorageTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.fsck.k9.preferences - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -private const val TEST_STRING_KEY = "s" -private const val TEST_STRING_VALUE = "y" -private const val TEST_INT_KEY = "i" -private const val TEST_INT_VALUE = "4" -private const val TEST_STRING_DEFAULT = "z" -private const val TEST_INT_DEFAULT = 2 -private val TEST_MAP = mapOf( - TEST_STRING_KEY to TEST_STRING_VALUE, - TEST_INT_KEY to TEST_INT_VALUE -) - -class StorageTest { - internal var storage = Storage() - - @Test - fun isEmpty() { - assertTrue(storage.isEmpty) - } - - @Test - fun isNotEmpty() { - storage.replaceAll(TEST_MAP) - assertFalse(storage.isEmpty) - } - - @Test - fun contains() { - storage.replaceAll(TEST_MAP) - assertTrue(storage.contains(TEST_STRING_KEY)) - } - - @Test - fun getString() { - storage.replaceAll(TEST_MAP) - assertEquals(TEST_STRING_VALUE, storage.getString(TEST_STRING_KEY, TEST_STRING_DEFAULT)) - } - - @Test - fun getString_default() { - assertTrue(storage.isEmpty) - assertEquals(TEST_STRING_DEFAULT, storage.getString(TEST_STRING_KEY, TEST_STRING_DEFAULT)) - } - - @Test - fun getAll() { - storage.replaceAll(TEST_MAP) - assertFalse(storage.isEmpty) - assertEquals(TEST_MAP, storage.all) - } - - @Test - fun replaceAll() { - storage.replaceAll(TEST_MAP) - storage.replaceAll(emptyMap()) - assertTrue(storage.isEmpty) - } - - @Test - fun getInteger() { - storage.replaceAll(TEST_MAP) - - assertEquals(Integer.parseInt(TEST_INT_VALUE), storage.getInt(TEST_INT_KEY, TEST_INT_DEFAULT)) - } - - @Test - fun getInteger_stringValue() { - storage.replaceAll(TEST_MAP) - - // TODO is this good behavior? - assertEquals(TEST_INT_DEFAULT, storage.getInt(TEST_STRING_KEY, TEST_INT_DEFAULT)) - } -} diff --git a/app/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d450f0dc49ca715b1a0a88a5aa746ee11e..0000000000000000000000000000000000000000 --- a/app/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline diff --git a/app/html-cleaner/.gitignore b/app/html-cleaner/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8 --- /dev/null +++ b/app/html-cleaner/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/html-cleaner/build.gradle b/app/html-cleaner/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..eff0c3492f96b7e5c5b867e0db82bea5f0f3e2b3 --- /dev/null +++ b/app/html-cleaner/build.gradle @@ -0,0 +1,14 @@ +apply plugin: 'java-library' +apply plugin: 'kotlin' + +java { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion +} + +dependencies { + implementation "org.jsoup:jsoup:${versions.jsoup}" + + testImplementation "junit:junit:${versions.junit}" + testImplementation "com.google.truth:truth:${versions.truth}" +} diff --git a/app/core/src/main/java/com/fsck/k9/message/html/BodyCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt similarity index 88% rename from app/core/src/main/java/com/fsck/k9/message/html/BodyCleaner.kt rename to app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt index 5cdd17ea99e8d5439175aff8a36b781b03988d29..0720118d385f4e428fcabe9bd4b9426c0ba39a19 100644 --- a/app/core/src/main/java/com/fsck/k9/message/html/BodyCleaner.kt +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt @@ -1,4 +1,4 @@ -package com.fsck.k9.message.html +package app.k9mail.html.cleaner import org.jsoup.nodes.Document import org.jsoup.safety.Cleaner @@ -43,10 +43,17 @@ internal class BodyCleaner { fun clean(dirtyDocument: Document): Document { val cleanedDocument = cleaner.clean(dirtyDocument) + copyDocumentType(dirtyDocument, cleanedDocument) copyBodyAttributes(dirtyDocument, cleanedDocument) return cleanedDocument } + private fun copyDocumentType(dirtyDocument: Document, cleanedDocument: Document) { + dirtyDocument.documentType()?.let { documentType -> + cleanedDocument.insertChildren(0, documentType) + } + } + private fun copyBodyAttributes(dirtyDocument: Document, cleanedDocument: Document) { val cleanedBody = cleanedDocument.body() for (attribute in dirtyDocument.body().attributes()) { diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt new file mode 100644 index 0000000000000000000000000000000000000000..54b9920d16d52ac1a24d3b0f6e9ced24c287e445 --- /dev/null +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt @@ -0,0 +1,75 @@ +package app.k9mail.html.cleaner + +import org.jsoup.nodes.DataNode +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode +import org.jsoup.parser.Tag +import org.jsoup.select.NodeTraversor +import org.jsoup.select.NodeVisitor + +private val ALLOWED_TAGS = listOf("style", "meta") + +internal class HeadCleaner { + fun clean(dirtyDocument: Document, cleanedDocument: Document) { + copySafeNodes(dirtyDocument.head(), cleanedDocument.head()) + } + + private fun copySafeNodes(source: Element, destination: Element) { + val cleaningVisitor = CleaningVisitor(source, destination) + NodeTraversor.traverse(cleaningVisitor, source) + } +} + +internal class CleaningVisitor( + private val root: Element, + private var destination: Element +) : NodeVisitor { + private var elementToSkip: Element? = null + + override fun head(source: Node, depth: Int) { + if (elementToSkip != null) return + + if (source is Element) { + if (isSafeTag(source)) { + val sourceTag = source.tagName() + val destinationAttributes = source.attributes().clone() + val destinationChild = Element(Tag.valueOf(sourceTag), source.baseUri(), destinationAttributes) + destination.appendChild(destinationChild) + destination = destinationChild + } else if (source !== root) { + elementToSkip = source + } + } else if (source is TextNode) { + val destinationText = TextNode(source.wholeText) + destination.appendChild(destinationText) + } else if (source is DataNode && isSafeTag(source.parent())) { + val destinationData = DataNode(source.wholeData) + destination.appendChild(destinationData) + } + } + + override fun tail(source: Node, depth: Int) { + if (source === elementToSkip) { + elementToSkip = null + } else if (source is Element && isSafeTag(source)) { + destination = destination.parent() + } + } + + private fun isSafeTag(node: Node): Boolean { + if (isMetaRefresh(node)) return false + + val tag = node.nodeName().lowercase() + return tag in ALLOWED_TAGS + } + + private fun isMetaRefresh(node: Node): Boolean { + val tag = node.nodeName().lowercase() + if (tag != "meta") return false + + val attributeValue = node.attributes().getIgnoreCase("http-equiv").trim().lowercase() + return attributeValue == "refresh" + } +} diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlHeadProvider.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlHeadProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..cf234cce33e59368157a3612fcbbc5802a9d877c --- /dev/null +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlHeadProvider.kt @@ -0,0 +1,5 @@ +package app.k9mail.html.cleaner + +interface HtmlHeadProvider { + val headHtml: String +} diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlProcessor.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..2da379b4eca4253b8a284bd31e8b3593fde01713 --- /dev/null +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlProcessor.kt @@ -0,0 +1,25 @@ +package app.k9mail.html.cleaner + +import org.jsoup.nodes.Document + +class HtmlProcessor(private val htmlHeadProvider: HtmlHeadProvider) { + private val htmlSanitizer = HtmlSanitizer() + + fun processForDisplay(html: String): String { + return htmlSanitizer.sanitize(html) + .addCustomHeadContents() + .toCompactString() + } + + private fun Document.addCustomHeadContents() = apply { + head().append(htmlHeadProvider.headHtml) + } + + private fun Document.toCompactString(): String { + outputSettings() + .prettyPrint(false) + .indentAmount(0) + + return html() + } +} diff --git a/app/core/src/main/java/com/fsck/k9/message/html/HtmlSanitizer.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlSanitizer.kt similarity index 76% rename from app/core/src/main/java/com/fsck/k9/message/html/HtmlSanitizer.kt rename to app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlSanitizer.kt index b302eafe698a63f016ae05433ebf8513a16353bf..d11bb30bdc6f41136713fbcb1192e77d455c4256 100644 --- a/app/core/src/main/java/com/fsck/k9/message/html/HtmlSanitizer.kt +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlSanitizer.kt @@ -1,13 +1,13 @@ -package com.fsck.k9.message.html +package app.k9mail.html.cleaner import org.jsoup.Jsoup import org.jsoup.nodes.Document -class HtmlSanitizer { +internal class HtmlSanitizer { private val headCleaner = HeadCleaner() private val bodyCleaner = BodyCleaner() - fun sanitize(html: String?): Document { + fun sanitize(html: String): Document { val dirtyDocument = Jsoup.parse(html) val cleanedDocument = bodyCleaner.clean(dirtyDocument) headCleaner.clean(dirtyDocument, cleanedDocument) diff --git a/app/core/src/test/java/com/fsck/k9/message/html/HtmlSanitizerTest.kt b/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt similarity index 90% rename from app/core/src/test/java/com/fsck/k9/message/html/HtmlSanitizerTest.kt rename to app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt index 0651e942b1aaf2bce2ff1830f0a2df674a05fda8..9ebc3857b5ea03d26b80fe57e2bbd075ea073238 100644 --- a/app/core/src/test/java/com/fsck/k9/message/html/HtmlSanitizerTest.kt +++ b/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt @@ -1,4 +1,4 @@ -package com.fsck.k9.message.html +package app.k9mail.html.cleaner import com.google.common.truth.Truth.assertThat import org.jsoup.nodes.Document @@ -376,7 +376,44 @@ class HtmlSanitizerTest { ) } - private fun Document.toCompactString() = HtmlProcessor.toCompactString(this) + @Test + fun `should keep HTML 5 doctype`() { + val html = + """ + + text + """.trimIndent().trimLineBreaks() + + val result = htmlSanitizer.sanitize(html) + + assertThat(result.toCompactString()).isEqualTo(html) + } + + @Test + fun `should keep HTML 4_01 doctype`() { + val html = + """ + + text + """.trimIndent().trimLineBreaks() + + val result = htmlSanitizer.sanitize(html) + + assertThat(result.toCompactString()).isEqualTo( + """ + + text + """.trimIndent().trimLineBreaks() + ) + } + + private fun Document.toCompactString(): String { + outputSettings() + .prettyPrint(false) + .indentAmount(0) + + return html() + } private fun String.trimLineBreaks() = replace("\n", "") } diff --git a/app/k9mail-jmap/build.gradle b/app/k9mail-jmap/build.gradle deleted file mode 100644 index d29aea28ab510dd3757b21624e4c8d6d45daed52..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/build.gradle +++ /dev/null @@ -1,135 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'org.jetbrains.kotlin.android' -apply plugin: 'kotlin-kapt' - -if (rootProject.testCoverage) { - apply plugin: 'jacoco' -} - -dependencies { - implementation project(":app:ui:legacy") - implementation project(":app:core") - implementation project(":app:storage") - implementation project(":app:crypto-openpgp") - implementation project(":backend:imap") - implementation project(":backend:pop3") - implementation project(":backend:webdav") - implementation project(":backend:jmap") - - implementation "androidx.appcompat:appcompat:${versions.androidxAppCompat}" - implementation "com.jakewharton.timber:timber:${versions.timber}" - implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}" - implementation "com.google.android.material:material:${versions.materialComponents}" - implementation "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}" - implementation "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidxLifecycle}" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}" - - implementation "com.github.bumptech.glide:glide:${versions.glide}" - kapt "com.github.bumptech.glide:compiler:${versions.glide}" - - // Required for DependencyInjectionTest to be able to resolve OpenPgpApiManager - testImplementation project(':plugins:openpgp-api-lib:openpgp-api') - - testImplementation "org.robolectric:robolectric:${versions.robolectric}" - testImplementation "junit:junit:${versions.junit}" - testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" - testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" - testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}" -} - -android { - compileSdkVersion buildConfig.compileSdk - buildToolsVersion buildConfig.buildTools - - defaultConfig { - applicationId "com.fsck.k9.jmap" - testApplicationId "com.fsck.k9.jmap.tests" - - versionCode 1 - versionName 'JMAP DEV' - - minSdkVersion buildConfig.minSdk - targetSdkVersion buildConfig.targetSdk - - generatedDensities = ['mdpi', 'hdpi', 'xhdpi'] - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - signingConfigs { - release - } - - buildTypes { - release { - if (project.hasProperty('storeFile')) { - signingConfig signingConfigs.release - } - - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - - buildConfigField "boolean", "DEVELOPER_MODE", "false" - } - - debug { - applicationIdSuffix ".debug" - testCoverageEnabled rootProject.testCoverage - - minifyEnabled false - - buildConfigField "boolean", "DEVELOPER_MODE", "true" - } - } - - // Do not abort build if lint finds errors - lintOptions { - abortOnError false - lintConfig file("$rootProject.projectDir/config/lint/lint.xml") - } - - packagingOptions { - exclude 'META-INF/DEPENDENCIES' - exclude 'META-INF/LICENSE' - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE' - exclude 'META-INF/NOTICE.txt' - exclude 'META-INF/README' - exclude 'LICENSE.txt' - exclude 'META-INF/*.kotlin_module' - } - - compileOptions { - sourceCompatibility javaVersion - targetCompatibility javaVersion - } - - kotlinOptions { - jvmTarget = kotlinJvmVersion - } - - buildFeatures { - dataBinding = true - } - - testOptions { - unitTests { - includeAndroidResources = true - } - } -} - -if (project.hasProperty('keyAlias')) { - android.signingConfigs.release.keyAlias = keyAlias -} -if (project.hasProperty('keyPassword')) { - android.signingConfigs.release.keyPassword = keyPassword -} -if (project.hasProperty('storeFile')) { - android.signingConfigs.release.storeFile = file(storeFile) -} -if (project.hasProperty('storePassword')) { - android.signingConfigs.release.storePassword = storePassword -} diff --git a/app/k9mail-jmap/proguard-rules.pro b/app/k9mail-jmap/proguard-rules.pro deleted file mode 100644 index 7e5879495390a93891cce30f1fa182e360ba8371..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/proguard-rules.pro +++ /dev/null @@ -1,52 +0,0 @@ -# Add project specific ProGuard rules here. - --dontobfuscate - -# Preserve the line number information for debugging stack traces. --keepattributes SourceFile,LineNumberTable - -# Library specific rules --dontnote android.net.http.* --dontnote org.apache.commons.codec.** --dontnote org.apache.http.** --dontnote com.squareup.moshi.** --dontnote com.github.amlcurran.showcaseview.** --dontnote de.cketti.safecontentresolver.** --dontnote com.tokenautocomplete.** --keep class rs.ltt.jmap.common.** {*;} - --dontwarn okio.** --dontwarn com.squareup.moshi.** - -# Glide --keep public class * extends com.bumptech.glide.module.AppGlideModule --keep public class * extends com.bumptech.glide.module.LibraryGlideModule --keep public enum com.bumptech.glide.load.ImageHeaderParser$** { - **[] $VALUES; - public *; -} - -# Project specific rules --dontnote com.fsck.k9.ui.messageview.** --dontnote com.fsck.k9.view.** - --keep public class org.openintents.openpgp.** - --keepclassmembers class * extends androidx.appcompat.widget.SearchView { - public (android.content.Context); -} - -# okhttp rules -# see: https://github.com/square/okhttp/blob/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro - -# JSR 305 annotations are for embedding nullability information. --dontwarn javax.annotation.** - -# A resource is loaded with a relative path so the package of this class must be preserved. --keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase - -# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. --dontwarn org.codehaus.mojo.animal_sniffer.* - -# OkHttp platform used only on JVM and when Conscrypt dependency is available. --dontwarn okhttp3.internal.platform.ConscryptPlatform diff --git a/app/k9mail-jmap/src/main/AndroidManifest.xml b/app/k9mail-jmap/src/main/AndroidManifest.xml deleted file mode 100644 index ec86eaf954d122000760d39dcf0e0821b426487b..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/AndroidManifest.xml +++ /dev/null @@ -1,300 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 deleted file mode 100644 index 1a2c819baba78a4593e3e362ad00d06e28ef8f72..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/App.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.fsck.k9 - -import android.app.Application -import com.fsck.k9.activity.MessageCompose -import com.fsck.k9.controller.MessagingController -import com.fsck.k9.ui.base.ThemeManager -import org.koin.android.ext.android.inject - -class App : Application() { - private val messagingController: MessagingController by inject() - private val messagingListenerProvider: MessagingListenerProvider by inject() - private val themeManager: ThemeManager by inject() - - override fun onCreate() { - Core.earlyInit() - - super.onCreate() - - DI.start(this, coreModules + uiModules + appModules) - - K9.init(this) - Core.init(this) - themeManager.init() - - messagingListenerProvider.listeners.forEach { listener -> - messagingController.addListener(listener) - } - } - - companion object { - val appConfig = AppConfig( - componentsToDisable = listOf(MessageCompose::class.java) - ) - } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/Dependencies.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/Dependencies.kt deleted file mode 100644 index bd33d9b7f0fa43eb48c2fd466279b013d9a27520..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/Dependencies.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.fsck.k9 - -import com.fsck.k9.backends.backendsModule -import com.fsck.k9.controller.ControllerExtension -import com.fsck.k9.crypto.EncryptionExtractor -import com.fsck.k9.crypto.openpgp.OpenPgpEncryptionExtractor -import com.fsck.k9.notification.notificationModule -import com.fsck.k9.preferences.K9StoragePersister -import com.fsck.k9.preferences.StoragePersister -import com.fsck.k9.resources.resourcesModule -import com.fsck.k9.storage.storageModule -import com.fsck.k9.ui.addaccount.uiAddAccountModule -import org.koin.core.qualifier.named -import org.koin.dsl.module - -private val mainAppModule = module { - single { App.appConfig } - single { MessagingListenerProvider(emptyList()) } - single(named("controllerExtensions")) { emptyList() } - single { OpenPgpEncryptionExtractor.newInstance() } - single { K9StoragePersister(get()) } -} - -val appModules = listOf( - mainAppModule, - notificationModule, - resourcesModule, - backendsModule, - storageModule, - uiAddAccountModule -) diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/MessagingListenerProvider.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/MessagingListenerProvider.kt deleted file mode 100644 index b81d41af8e2149aa74389edaf9d1255899622846..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/MessagingListenerProvider.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.fsck.k9 - -import com.fsck.k9.controller.MessagingListener - -class MessagingListenerProvider(val listeners: List) diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/AndroidAlarmManager.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/AndroidAlarmManager.kt deleted file mode 100644 index a39569458940499627c70fdc7aad56ba681f95d6..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/AndroidAlarmManager.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.fsck.k9.backends - -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.Build -import android.os.SystemClock -import com.fsck.k9.backend.imap.SystemAlarmManager -import com.fsck.k9.helper.AlarmManagerCompat -import java.util.concurrent.atomic.AtomicReference -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import timber.log.Timber - -private const val ALARM_ACTION = "com.fsck.k9.backends.ALARM" -private const val REQUEST_CODE = 1 - -private typealias Callback = () -> Unit - -class AndroidAlarmManager( - private val context: Context, - private val alarmManager: AlarmManagerCompat, - backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO -) : SystemAlarmManager { - private val coroutineScope = CoroutineScope(backgroundDispatcher) - - private val pendingIntent: PendingIntent = run { - val intent = Intent(ALARM_ACTION).apply { - setPackage(context.packageName) - } - val flags = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0 - - PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flags) - } - - private val callback = AtomicReference(null) - - init { - val intentFilter = IntentFilter(ALARM_ACTION) - context.registerReceiver( - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - val callback = callback.getAndSet(null) - if (callback == null) { - Timber.w("Alarm triggered but 'callback' was null") - } else { - coroutineScope.launch { - callback.invoke() - } - } - } - }, - intentFilter - ) - } - - override fun setAlarm(triggerTime: Long, callback: Callback) { - this.callback.set(callback) - alarmManager.scheduleAlarm(triggerTime, pendingIntent) - } - - override fun cancelAlarm() { - callback.set(null) - alarmManager.cancelAlarm(pendingIntent) - } - - override fun now(): Long = SystemClock.elapsedRealtime() -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt deleted file mode 100644 index e5145469acf64b184d13f795422abc9fa0656f30..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.fsck.k9.backends - -import android.content.Context -import android.net.ConnectivityManager -import com.fsck.k9.Account -import com.fsck.k9.backend.BackendFactory -import com.fsck.k9.backend.api.Backend -import com.fsck.k9.backend.imap.ImapBackend -import com.fsck.k9.backend.imap.ImapPushConfigProvider -import com.fsck.k9.mail.NetworkType -import com.fsck.k9.mail.oauth.OAuth2TokenProvider -import com.fsck.k9.mail.power.PowerManager -import com.fsck.k9.mail.ssl.TrustedSocketFactory -import com.fsck.k9.mail.store.imap.IdleRefreshManager -import com.fsck.k9.mail.store.imap.ImapStore -import com.fsck.k9.mail.store.imap.ImapStoreConfig -import com.fsck.k9.mail.transport.smtp.SmtpTransport -import com.fsck.k9.mailstore.K9BackendStorageFactory -import com.fsck.k9.preferences.AccountManager -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map - -class ImapBackendFactory( - private val context: Context, - private val accountManager: AccountManager, - private val powerManager: PowerManager, - private val idleRefreshManager: IdleRefreshManager, - private val backendStorageFactory: K9BackendStorageFactory, - private val trustedSocketFactory: TrustedSocketFactory -) : BackendFactory { - override fun createBackend(account: Account): Backend { - val accountName = account.displayName - val backendStorage = backendStorageFactory.createBackendStorage(account) - val imapStore = createImapStore(account) - val pushConfigProvider = createPushConfigProvider(account) - val smtpTransport = createSmtpTransport(account) - - return ImapBackend( - accountName, - backendStorage, - imapStore, - powerManager, - idleRefreshManager, - pushConfigProvider, - smtpTransport - ) - } - - private fun createImapStore(account: Account): ImapStore { - val oAuth2TokenProvider: OAuth2TokenProvider? = null - val config = createImapStoreConfig(account) - return ImapStore.create( - account.incomingServerSettings, - config, - trustedSocketFactory, - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager, - oAuth2TokenProvider - ) - } - - private fun createImapStoreConfig(account: Account): ImapStoreConfig { - return object : ImapStoreConfig { - override val logLabel - get() = account.description - - override fun isSubscribedFoldersOnly() = account.isSubscribedFoldersOnly - - override fun useCompression(type: NetworkType) = account.useCompression(type) - } - } - - private fun createSmtpTransport(account: Account): SmtpTransport { - val serverSettings = account.outgoingServerSettings - val oauth2TokenProvider: OAuth2TokenProvider? = null - return SmtpTransport(serverSettings, trustedSocketFactory, oauth2TokenProvider) - } - - private fun createPushConfigProvider(account: Account) = object : ImapPushConfigProvider { - override val maxPushFoldersFlow: Flow - get() = accountManager.getAccountFlow(account.uuid) - .map { it.maxPushFolders } - .distinctUntilChanged() - - override val idleRefreshMinutesFlow: Flow - get() = accountManager.getAccountFlow(account.uuid) - .map { it.idleRefreshMinutes } - .distinctUntilChanged() - } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/JmapAccountCreator.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/JmapAccountCreator.kt deleted file mode 100644 index ac23638f4d331c9fb086bb7cf56de98409dfcde8..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/JmapAccountCreator.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.fsck.k9.backends - -import com.fsck.k9.Account -import com.fsck.k9.Preferences -import com.fsck.k9.account.AccountCreator -import com.fsck.k9.backend.BackendManager -import com.fsck.k9.backend.jmap.JmapDiscoveryResult.JmapAccount -import com.fsck.k9.mail.AuthType -import com.fsck.k9.mail.ConnectionSecurity -import com.fsck.k9.mail.FolderType -import com.fsck.k9.mail.ServerSettings -import com.fsck.k9.mailstore.LocalStoreProvider - -class JmapAccountCreator( - private val preferences: Preferences, - private val backendManager: BackendManager, - private val accountCreator: AccountCreator, - private val localStoreProvider: LocalStoreProvider -) { - fun createAccount(emailAddress: String, password: String, jmapAccount: JmapAccount) { - val serverSettings = createServerSettings(emailAddress, password, jmapAccount) - - val account = preferences.newAccount().apply { - email = emailAddress - description = jmapAccount.name - incomingServerSettings = serverSettings - outgoingServerSettings = serverSettings - - chipColor = accountCreator.pickColor() - deletePolicy = Account.DeletePolicy.ON_DELETE - } - - createOutboxFolder(account) - preferences.saveAccount(account) - - fetchFolderList(account) - } - - private fun createServerSettings(emailAddress: String, password: String, jmapAccount: JmapAccount): ServerSettings { - return ServerSettings( - "jmap", - null, - 433, - ConnectionSecurity.SSL_TLS_REQUIRED, - AuthType.PLAIN, - emailAddress, - password, - null, - mapOf("accountId" to jmapAccount.accountId) - ) - } - - private fun createOutboxFolder(account: Account) { - val localStore = localStoreProvider.getInstance(account) - account.outboxFolderId = localStore.createLocalFolder(Account.OUTBOX_NAME, FolderType.OUTBOX) - } - - private fun fetchFolderList(account: Account) { - val backend = backendManager.getBackend(account) - backend.refreshFolderList() - } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/JmapBackendFactory.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/JmapBackendFactory.kt deleted file mode 100644 index c6b52e377c8884f30d4fe665841d80b21b20f3e1..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/JmapBackendFactory.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.fsck.k9.backends - -import com.fsck.k9.Account -import com.fsck.k9.backend.BackendFactory -import com.fsck.k9.backend.api.Backend -import com.fsck.k9.backend.jmap.JmapBackend -import com.fsck.k9.backend.jmap.JmapConfig -import com.fsck.k9.mailstore.K9BackendStorageFactory - -class JmapBackendFactory( - private val backendStorageFactory: K9BackendStorageFactory, - private val okHttpClientProvider: OkHttpClientProvider -) : BackendFactory { - override fun createBackend(account: Account): Backend { - val backendStorage = backendStorageFactory.createBackendStorage(account) - val okHttpClient = okHttpClientProvider.getOkHttpClient() - - val serverSettings = account.incomingServerSettings - val jmapConfig = JmapConfig( - username = serverSettings.username, - password = serverSettings.password!!, - baseUrl = serverSettings.host, - accountId = serverSettings.extra["accountId"]!! - ) - - return JmapBackend(backendStorage, okHttpClient, jmapConfig) - } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/KoinModule.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/KoinModule.kt deleted file mode 100644 index 9a4d7ba32b7448989aff37de346135addc43f548..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/KoinModule.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.fsck.k9.backends - -import com.fsck.k9.backend.BackendManager -import com.fsck.k9.backend.imap.BackendIdleRefreshManager -import com.fsck.k9.backend.imap.SystemAlarmManager -import com.fsck.k9.backend.jmap.JmapAccountDiscovery -import com.fsck.k9.mail.store.imap.IdleRefreshManager -import org.koin.dsl.module - -val backendsModule = module { - single { - BackendManager( - mapOf( - "imap" to get(), - "pop3" to get(), - "webdav" to get(), - "jmap" to get() - ) - ) - } - single { - ImapBackendFactory( - context = get(), - accountManager = get(), - powerManager = get(), - idleRefreshManager = get(), - backendStorageFactory = get(), - trustedSocketFactory = get() - ) - } - single { AndroidAlarmManager(context = get(), alarmManager = get()) } - single { BackendIdleRefreshManager(alarmManager = get()) } - single { Pop3BackendFactory(get(), get()) } - single { WebDavBackendFactory(get(), get(), get()) } - single { JmapBackendFactory(get(), get()) } - factory { JmapAccountDiscovery() } - factory { JmapAccountCreator(get(), get(), get(), get()) } - single { OkHttpClientProvider() } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/OkHttpClientProvider.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/OkHttpClientProvider.kt deleted file mode 100644 index ed1dbd14f36c8ec346af7f43a3465789bf8e4c91..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/OkHttpClientProvider.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.fsck.k9.backends - -import okhttp3.OkHttpClient - -class OkHttpClientProvider { - private var okHttpClient: OkHttpClient? = null - - @Synchronized - fun getOkHttpClient(): OkHttpClient { - return okHttpClient ?: createOkHttpClient().also { okHttpClient = it } - } - - private fun createOkHttpClient(): OkHttpClient { - return OkHttpClient.Builder().build() - } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/Pop3BackendFactory.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/Pop3BackendFactory.kt deleted file mode 100644 index 4550f5f1759752162b7d894296b695ee4a2a0b49..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/Pop3BackendFactory.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.fsck.k9.backends - -import com.fsck.k9.Account -import com.fsck.k9.backend.BackendFactory -import com.fsck.k9.backend.api.Backend -import com.fsck.k9.backend.pop3.Pop3Backend -import com.fsck.k9.mail.oauth.OAuth2TokenProvider -import com.fsck.k9.mail.ssl.TrustedSocketFactory -import com.fsck.k9.mail.store.pop3.Pop3Store -import com.fsck.k9.mail.transport.smtp.SmtpTransport -import com.fsck.k9.mailstore.K9BackendStorageFactory - -class Pop3BackendFactory( - private val backendStorageFactory: K9BackendStorageFactory, - private val trustedSocketFactory: TrustedSocketFactory -) : BackendFactory { - override fun createBackend(account: Account): Backend { - val accountName = account.displayName - val backendStorage = backendStorageFactory.createBackendStorage(account) - val pop3Store = createPop3Store(account) - val smtpTransport = createSmtpTransport(account) - return Pop3Backend(accountName, backendStorage, pop3Store, smtpTransport) - } - - private fun createPop3Store(account: Account): Pop3Store { - val serverSettings = account.incomingServerSettings - return Pop3Store(serverSettings, trustedSocketFactory) - } - - private fun createSmtpTransport(account: Account): SmtpTransport { - val serverSettings = account.outgoingServerSettings - val oauth2TokenProvider: OAuth2TokenProvider? = null - return SmtpTransport(serverSettings, trustedSocketFactory, oauth2TokenProvider) - } -} 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 deleted file mode 100644 index 0098b96e468d38b506cb0ea82f36c08c77af6d4b..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.fsck.k9.backends - -import com.fsck.k9.Account -import com.fsck.k9.backend.BackendFactory -import com.fsck.k9.backend.api.Backend -import com.fsck.k9.backend.webdav.WebDavBackend -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.FolderRepository -import com.fsck.k9.mailstore.K9BackendStorageFactory - -class WebDavBackendFactory( - private val backendStorageFactory: K9BackendStorageFactory, - private val trustManagerFactory: TrustManagerFactory, - private val folderRepository: FolderRepository -) : BackendFactory { - override fun createBackend(account: Account): Backend { - val accountName = account.displayName - val backendStorage = backendStorageFactory.createBackendStorage(account) - val serverSettings = account.incomingServerSettings - val draftsFolderProvider = createDraftsFolderProvider(account) - val webDavStore = WebDavStore(trustManagerFactory, serverSettings, draftsFolderProvider) - val webDavTransport = WebDavTransport(trustManagerFactory, serverSettings, draftsFolderProvider) - return WebDavBackend(accountName, backendStorage, webDavStore, webDavTransport) - } - - private fun createDraftsFolderProvider(account: Account): DraftsFolderProvider { - return DraftsFolderProvider { - val draftsFolderId = account.draftsFolderId ?: error("No Drafts folder configured") - folderRepository.getFolderServerId(account, draftsFolderId) ?: error("Couldn't find local Drafts folder") - } - } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/glide/K9AppGlideModule.java b/app/k9mail-jmap/src/main/java/com/fsck/k9/glide/K9AppGlideModule.java deleted file mode 100644 index f37999f7f1584bfac283969427d3a52ac79e3163..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/glide/K9AppGlideModule.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.fsck.k9.glide; - -import com.bumptech.glide.annotation.GlideModule; -import com.bumptech.glide.module.AppGlideModule; - -@GlideModule -public class K9AppGlideModule extends AppGlideModule { -} 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 deleted file mode 100644 index 33b59551f69c3eacc2074074612600aee24abc87..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt +++ /dev/null @@ -1,234 +0,0 @@ -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 deleted file mode 100644 index 0cea5e4c1112547859725afd900dbf15a9803be0..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationResourceProvider.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.fsck.k9.notification - -import android.content.Context -import com.fsck.k9.jmap.R - -class K9NotificationResourceProvider(private val context: Context) : NotificationResourceProvider { - override val iconWarning: Int = R.drawable.notification_icon_warning - override val iconMarkAsRead: Int = R.drawable.notification_action_mark_as_read - override val iconDelete: Int = R.drawable.notification_action_delete - override val iconReply: Int = R.drawable.notification_action_reply - override val iconNewMail: Int = R.drawable.notification_icon_new_mail - override val iconSendingMail: Int = R.drawable.notification_icon_check_mail - override val iconCheckingMail: Int = R.drawable.notification_icon_check_mail - override val wearIconMarkAsRead: Int = R.drawable.notification_action_mark_as_read - override val wearIconDelete: Int = R.drawable.notification_action_delete - override val wearIconArchive: Int = R.drawable.notification_action_archive - override val wearIconReplyAll: Int = R.drawable.notification_action_reply - override val wearIconMarkAsSpam: Int = R.drawable.notification_action_mark_as_spam - - override val pushChannelName: String - get() = context.getString(R.string.notification_channel_push_title) - override val pushChannelDescription: String - get() = context.getString(R.string.notification_channel_push_description) - override val messagesChannelName: String - get() = context.getString(R.string.notification_channel_messages_title) - override val messagesChannelDescription: String - get() = context.getString(R.string.notification_channel_messages_description) - override val miscellaneousChannelName: String - get() = context.getString(R.string.notification_channel_miscellaneous_title) - override val miscellaneousChannelDescription: String - get() = context.getString(R.string.notification_channel_miscellaneous_description) - - override fun authenticationErrorTitle(): String = - context.getString(R.string.notification_authentication_error_title) - - 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) - - override fun certificateErrorBody(): String = context.getString(R.string.notification_certificate_error_text) - - override fun newMailTitle(): String = context.getString(R.string.notification_new_title) - - override fun newMailUnreadMessageCount(unreadMessageCount: Int, accountName: String): String = - context.getString(R.string.notification_new_one_account_fmt, unreadMessageCount, accountName) - - override fun newMessagesTitle(newMessagesCount: Int): String = - context.resources.getQuantityString( - R.plurals.notification_new_messages_title, - newMessagesCount, newMessagesCount - ) - - override fun additionalMessages(overflowMessagesCount: Int, accountName: String): String = - context.getString(R.string.notification_additional_messages, overflowMessagesCount, accountName) - - override fun previewEncrypted(): String = context.getString(R.string.preview_encrypted) - - override fun noSubject(): String = context.getString(R.string.general_no_subject) - - override fun recipientDisplayName(recipientDisplayName: String): String = - context.getString(R.string.message_to_fmt, recipientDisplayName) - - override fun noSender(): String = context.getString(R.string.general_no_sender) - - override fun sendFailedTitle(): String = context.getString(R.string.send_failure_subject) - - override fun sendingMailTitle(): String = context.getString(R.string.notification_bg_send_title) - - override fun sendingMailBody(accountName: String): String = - context.getString(R.string.notification_bg_send_ticker, accountName) - - override fun checkingMailTicker(accountName: String, folderName: String): String = - context.getString(R.string.notification_bg_sync_ticker, accountName, folderName) - - override fun checkingMailTitle(): String = - context.getString(R.string.notification_bg_sync_title) - - override fun checkingMailSeparator(): String = - context.getString(R.string.notification_bg_title_separator) - - override fun actionMarkAsRead(): String = context.getString(R.string.notification_action_mark_as_read) - - override fun actionMarkAllAsRead(): String = context.getString(R.string.notification_action_mark_all_as_read) - - override fun actionDelete(): String = context.getString(R.string.notification_action_delete) - - override fun actionDeleteAll(): String = context.getString(R.string.notification_action_delete_all) - - override fun actionReply(): String = context.getString(R.string.notification_action_reply) - - override fun actionArchive(): String = context.getString(R.string.notification_action_archive) - - override fun actionArchiveAll(): String = context.getString(R.string.notification_action_archive_all) - - override fun actionMarkAsSpam(): String = context.getString(R.string.notification_action_spam) -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt deleted file mode 100644 index 95fe647781f64f1e73fda0632963f116d58e0a21..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.fsck.k9.notification - -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.mailstore.LocalFolder -import com.fsck.k9.mailstore.LocalMessage - -class K9NotificationStrategy(val contacts: Contacts) : NotificationStrategy { - - override fun shouldNotifyForMessage( - account: Account, - localFolder: LocalFolder, - message: LocalMessage, - isOldMessage: Boolean - ): Boolean { - - // If we don't even have an account name, don't show the notification. - // (This happens during initial account setup) - if (account.name == null) { - return false - } - - if (K9.isQuietTime && !K9.isNotificationDuringQuietTimeEnabled) { - return false - } - - // Do not notify if the user does not have notifications enabled or if the message has - // been read. - if (!account.isNotifyNewMail || message.isSet(Flag.SEEN) || isOldMessage) { - return false - } - - val aDisplayMode = account.folderDisplayMode - val aNotifyMode = account.folderNotifyNewMailMode - val fDisplayClass = localFolder.displayClass - val fNotifyClass = localFolder.notifyClass - - if (LocalFolder.isModeMismatch(aDisplayMode, fDisplayClass)) { - // Never notify a folder that isn't displayed - return false - } - - if (LocalFolder.isModeMismatch(aNotifyMode, fNotifyClass)) { - // Do not notify folders in the wrong class - return false - } - - // No notification for new messages in Trash, Drafts, Spam or Sent folder. - val folder = message.folder - if (folder != null) { - val folderId = folder.databaseId - if (folderId == account.trashFolderId || - folderId == account.draftsFolderId || - folderId == account.spamFolderId || - folderId == account.sentFolderId - ) { - return false - } - } - - // Don't notify if the sender address matches one of our identities and the user chose not - // to be notified for such messages. - return if (account.isAnIdentity(message.from) && !account.isNotifySelfNewMail) { - false - } else !account.isNotifyContactsMailOnly || contacts.isAnyInContacts(message.from) - } -} 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 deleted file mode 100644 index ac5ee21308f2bdaf0674ce923cc6f055dfc67b8b..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/notification/KoinModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.fsck.k9.notification - -import org.koin.dsl.module - -val notificationModule = module { - 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/K9AutocryptStringProvider.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/resources/K9AutocryptStringProvider.kt deleted file mode 100644 index eb6ad899ba98b01ab8e1993fa6d04ae856001cf9..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/resources/K9AutocryptStringProvider.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.fsck.k9.resources - -import android.content.Context -import com.fsck.k9.autocrypt.AutocryptStringProvider -import com.fsck.k9.jmap.R - -class K9AutocryptStringProvider(private val context: Context) : AutocryptStringProvider { - override fun transferMessageSubject(): String = context.getString(R.string.ac_transfer_msg_subject) - override fun transferMessageBody(): String = context.getString(R.string.ac_transfer_msg_body) -} 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 deleted file mode 100644 index 6c46f88f2dd11e5f409e50869e6782b8775cbca6..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/resources/K9CoreResourceProvider.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.fsck.k9.resources - -import android.content.Context -import com.fsck.k9.CoreResourceProvider -import com.fsck.k9.jmap.R -import com.fsck.k9.notification.PushNotificationState - -class K9CoreResourceProvider(private val context: Context) : CoreResourceProvider { - override fun defaultSignature(): String = context.getString(R.string.default_signature) - override fun defaultIdentityDescription(): String = context.getString(R.string.default_identity_description) - - 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) - - override fun messageHeaderFrom(): String = context.getString(R.string.message_compose_quote_header_from) - override fun messageHeaderTo(): String = context.getString(R.string.message_compose_quote_header_to) - override fun messageHeaderCc(): String = context.getString(R.string.message_compose_quote_header_cc) - override fun messageHeaderDate(): String = context.getString(R.string.message_compose_quote_header_send_date) - override fun messageHeaderSubject(): String = context.getString(R.string.message_compose_quote_header_subject) - override fun messageHeaderSeparator(): String = context.getString(R.string.message_compose_quote_header_separator) - - override fun noSubject(): String = context.getString(R.string.general_no_subject) - - override fun userAgent(): String = context.getString(R.string.message_header_mua) - override fun encryptedSubject(): String = context.getString(R.string.encrypted_subject) - - override fun replyHeader(sender: String): String = - context.getString(R.string.message_compose_reply_header_fmt, sender) - - override fun replyHeader(sender: String, sentDate: String): String = - context.getString(R.string.message_compose_reply_header_fmt_with_date, sentDate, sender) - - override fun searchUnifiedInboxTitle(): String = context.getString(R.string.integrated_inbox_title) - override fun searchUnifiedInboxDetail(): String = context.getString(R.string.integrated_inbox_detail) - - override fun outboxFolderName(): String = context.getString(R.string.special_mailbox_name_outbox) - - override val iconPushNotification: Int = R.drawable.ic_push_notification - - override fun pushNotificationText(notificationState: PushNotificationState): String { - val resId = when (notificationState) { - PushNotificationState.INITIALIZING -> R.string.push_notification_state_initializing - PushNotificationState.LISTENING -> R.string.push_notification_state_listening - PushNotificationState.WAIT_BACKGROUND_SYNC -> R.string.push_notification_state_wait_background_sync - PushNotificationState.WAIT_NETWORK -> R.string.push_notification_state_wait_network - } - return context.getString(resId) - } - - override fun pushNotificationInfoText(): String = context.getString(R.string.push_notification_info) -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/resources/KoinModule.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/resources/KoinModule.kt deleted file mode 100644 index 61bf6af4ba2ad0e02897f0ab1e7e860c985c5ee5..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/resources/KoinModule.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.fsck.k9.resources - -import com.fsck.k9.CoreResourceProvider -import com.fsck.k9.autocrypt.AutocryptStringProvider -import com.fsck.k9.ui.base.ThemeProvider -import org.koin.dsl.module - -val resourcesModule = module { - single { K9CoreResourceProvider(get()) } - single { K9AutocryptStringProvider(get()) } - single { com.fsck.k9.K9ThemeProvider() } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/DataBindingAdapters.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/DataBindingAdapters.kt deleted file mode 100644 index da7176516964b3bbb13c252312b1a972a85739c7..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/DataBindingAdapters.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.fsck.k9.ui - -import android.view.View -import androidx.core.view.isVisible -import androidx.databinding.BindingAdapter -import com.google.android.material.textfield.TextInputLayout - -@BindingAdapter("isVisible") -fun setVisibility(view: View, value: Boolean) { - view.isVisible = value -} - -@BindingAdapter("error") -fun setError(view: TextInputLayout, value: Int?) { - if (value == null) { - view.error = null - } else { - val errorString = view.context.getString(value) - view.error = errorString - } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/AddAccountActivity.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/AddAccountActivity.kt deleted file mode 100644 index bade75d0f3c20947e36858cf45b19587122c600b..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/AddAccountActivity.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.fsck.k9.ui.addaccount - -import android.os.Bundle -import androidx.navigation.NavController -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.setupActionBarWithNavController -import com.fsck.k9.jmap.R -import com.fsck.k9.ui.base.K9Activity -import com.fsck.k9.ui.base.extensions.findNavController - -class AddAccountActivity : K9Activity() { - private lateinit var navController: NavController - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setLayout(R.layout.activity_add_account) - - initializeActionBar() - } - - private fun initializeActionBar() { - val appBarConfiguration = AppBarConfiguration(topLevelDestinationIds = setOf(R.id.addJmapAccountScreen)) - - navController = findNavController(R.id.nav_host_fragment) - setupActionBarWithNavController(navController, appBarConfiguration) - } - - override fun onSupportNavigateUp(): Boolean { - return navController.navigateUp() || super.onSupportNavigateUp() - } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/AddAccountFragment.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/AddAccountFragment.kt deleted file mode 100644 index 4fba3d41891607471149ce05309b573cd8076402..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/AddAccountFragment.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.fsck.k9.ui.addaccount - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import com.fsck.k9.jmap.R -import com.fsck.k9.jmap.databinding.FragmentAddAccountBinding -import com.fsck.k9.ui.observeNotNull -import org.koin.androidx.viewmodel.ext.android.viewModel - -class AddAccountFragment : Fragment() { - private val viewModel: AddAccountViewModel by viewModel() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - viewModel.getActionEvents().observeNotNull(this) { handleActionEvents(it) } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val binding = FragmentAddAccountBinding.inflate(inflater, container, false) - binding.lifecycleOwner = this - binding.viewModel = viewModel - - return binding.root - } - - private fun handleActionEvents(action: Action) { - when (action) { - is Action.GoToMessageList -> goToMessageList() - } - } - - private fun goToMessageList() { - findNavController().navigate(R.id.action_addJmapAccountScreen_to_messageListScreen) - } -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/AddAccountViewModel.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/AddAccountViewModel.kt deleted file mode 100644 index c97ca4675343cac1d752474d1112296bd42f6891..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/AddAccountViewModel.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.fsck.k9.ui.addaccount - -import androidx.annotation.StringRes -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.fsck.k9.EmailAddressValidator -import com.fsck.k9.backend.jmap.JmapAccountDiscovery -import com.fsck.k9.backend.jmap.JmapDiscoveryResult -import com.fsck.k9.backend.jmap.JmapDiscoveryResult.JmapAccount -import com.fsck.k9.backends.JmapAccountCreator -import com.fsck.k9.helper.SingleLiveEvent -import com.fsck.k9.helper.measureRealtimeMillisWithResult -import com.fsck.k9.jmap.R -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class AddAccountViewModel( - private val emailAddressValidator: EmailAddressValidator, - private val jmapAccountDiscovery: JmapAccountDiscovery, - private val jmapAccountCreator: JmapAccountCreator -) : ViewModel() { - val emailAddress = MutableLiveData() - val emailAddressError = MutableLiveData() - val password = MutableLiveData() - val passwordError = MutableLiveData() - val setupErrorText: MutableLiveData = createMutableLiveData(R.string.empty_string) - val isInputEnabled: MutableLiveData = createMutableLiveData(true) - val isNextButtonEnabled: MutableLiveData = createMutableLiveData(true) - val isProgressBarVisible: MutableLiveData = createMutableLiveData(false) - private val actionLiveData = SingleLiveEvent() - - init { - Transformations.distinctUntilChanged(emailAddress).observeForever { resetEmailAddressError() } - Transformations.distinctUntilChanged(password).observeForever { resetPasswordError() } - } - - fun getActionEvents(): LiveData = actionLiveData - - fun onNextButtonClicked() { - discoverServerSettings() - } - - private fun discoverServerSettings() { - val emailAddress = this.emailAddress.value?.trim() ?: "" - val password = this.password.value ?: "" - - if (!emailAddressValidator.isValidAddressOnly(emailAddress)) { - displayEmailAddressError(R.string.add_account__email_address_error) - return - } - - showDiscoveryProgressBar() - - viewModelScope.launch { - val (elapsed, discoveryResult) = measureRealtimeMillisWithResult { - withContext(Dispatchers.IO) { - jmapAccountDiscovery.discover(emailAddress, password) - } - } - - if (elapsed < MIN_PROGRESS_DURATION) { - delay(MIN_PROGRESS_DURATION - elapsed) - } - - if (discoveryResult is JmapAccount) { - createAccount(emailAddress, password, discoveryResult) - } else { - displayDiscoveryError(discoveryResult) - hideDiscoveryProgressBar() - } - } - } - - private suspend fun createAccount(emailAddress: String, password: String, jmapAccount: JmapAccount) { - GlobalScope.launch(Dispatchers.IO) { - jmapAccountCreator.createAccount(emailAddress, password, jmapAccount) - }.join() - - sendActionEvent(Action.GoToMessageList) - } - - private fun displayDiscoveryError(discoveryResult: JmapDiscoveryResult) { - when (discoveryResult) { - is JmapDiscoveryResult.GenericFailure -> { - displayError(R.string.add_account__generic_failure) - } - is JmapDiscoveryResult.NoEmailAccountFoundFailure -> { - displayError(R.string.add_account__no_email_account_found) - } - is JmapDiscoveryResult.AuthenticationFailure -> { - displayPasswordError(R.string.add_account__password_error) - } - is JmapDiscoveryResult.EndpointNotFoundFailure -> { - displayError(R.string.add_account__jmap_server_not_found) - } - } - } - - @Suppress("SameParameterValue") - private fun displayEmailAddressError(@StringRes error: Int) { - emailAddressError.value = error - } - - private fun resetEmailAddressError() { - emailAddressError.value = null - } - - @Suppress("SameParameterValue") - private fun displayPasswordError(@StringRes error: Int) { - passwordError.value = error - } - - private fun resetPasswordError() { - passwordError.value = null - } - - private fun showDiscoveryProgressBar() { - isInputEnabled.value = false - isProgressBarVisible.value = true - isNextButtonEnabled.value = false - setupErrorText.value = R.string.empty_string - } - - private fun hideDiscoveryProgressBar() { - isInputEnabled.value = true - isProgressBarVisible.value = false - isNextButtonEnabled.value = true - } - - private fun displayError(@StringRes error: Int) { - setupErrorText.value = error - } - - private fun sendActionEvent(action: Action) { - actionLiveData.value = action - } - - private fun createMutableLiveData(initialValue: T): MutableLiveData { - return MutableLiveData().apply { - value = initialValue - } - } - - companion object { - private const val MIN_PROGRESS_DURATION = 500 - } -} - -sealed class Action { - object GoToMessageList : Action() -} diff --git a/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/KoinModule.kt b/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/KoinModule.kt deleted file mode 100644 index 0efa1daeccb9daaf93d23b410a50d5b20ec33f74..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/java/com/fsck/k9/ui/addaccount/KoinModule.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.fsck.k9.ui.addaccount - -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.dsl.module - -val uiAddAccountModule = module { - viewModel { AddAccountViewModel(get(), get(), get()) } -} diff --git a/app/k9mail-jmap/src/main/res/layout/activity_add_account.xml b/app/k9mail-jmap/src/main/res/layout/activity_add_account.xml deleted file mode 100644 index 57258b068422727b72dfe424b24656751207e16f..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/res/layout/activity_add_account.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - diff --git a/app/k9mail-jmap/src/main/res/layout/fragment_add_account.xml b/app/k9mail-jmap/src/main/res/layout/fragment_add_account.xml deleted file mode 100644 index 2a4a672b917afa1cb767ba5583b80c4a2d8f913f..0000000000000000000000000000000000000000 --- a/app/k9mail-jmap/src/main/res/layout/fragment_add_account.xml +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -