diff --git a/app/autodiscovery/api/build.gradle b/app/autodiscovery/api/build.gradle index 13eabe22f..aa86ea20a 100644 --- a/app/autodiscovery/api/build.gradle +++ b/app/autodiscovery/api/build.gradle @@ -1,6 +1,8 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} dependencies { api project(":mail:common") diff --git a/app/autodiscovery/providersxml/build.gradle b/app/autodiscovery/providersxml/build.gradle index 1ac525346..65ba3c587 100644 --- a/app/autodiscovery/providersxml/build.gradle +++ b/app/autodiscovery/providersxml/build.gradle @@ -1,23 +1,25 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} dependencies { implementation project(":app:core") implementation project(":mail:common") implementation project(":app:autodiscovery:api") - implementation "com.jakewharton.timber:timber:${versions.timber}" + implementation libs.timber testImplementation project(':app:testing') testImplementation project(":backend:imap") - testImplementation "org.robolectric:robolectric:${versions.robolectric}" - testImplementation "androidx.test:core:${versions.androidxTestCore}" - testImplementation "junit:junit:${versions.junit}" - testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-inline:${versions.mockito}" - testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" - testImplementation "io.insert-koin:koin-test:${versions.koin}" - testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}" + testImplementation libs.robolectric + testImplementation libs.androidx.test.core + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.inline + testImplementation libs.mockito.kotlin + testImplementation libs.koin.test + testImplementation libs.koin.test.junit4 } android { diff --git a/app/autodiscovery/srvrecords/build.gradle b/app/autodiscovery/srvrecords/build.gradle index 6ef976c06..70202b62f 100644 --- a/app/autodiscovery/srvrecords/build.gradle +++ b/app/autodiscovery/srvrecords/build.gradle @@ -1,14 +1,16 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} dependencies { api project(":app:autodiscovery:api") - implementation "org.minidns:minidns-hla:${versions.minidns}" + implementation libs.minidns.hla - testImplementation "junit:junit:${versions.junit}" - testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-inline:${versions.mockito}" - testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.inline + testImplementation libs.mockito.kotlin } diff --git a/app/autodiscovery/thunderbird/build.gradle b/app/autodiscovery/thunderbird/build.gradle index 6f76f4e1e..4ef0ba734 100644 --- a/app/autodiscovery/thunderbird/build.gradle +++ b/app/autodiscovery/thunderbird/build.gradle @@ -1,16 +1,18 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} dependencies { api project(":app:autodiscovery:api") - compileOnly 'com.github.cketti:xmlpull-extracted-from-android:1.0' - implementation "com.squareup.okhttp3:okhttp:${versions.okhttp}" + compileOnly libs.xmlpull + implementation libs.okhttp - testImplementation "junit:junit:${versions.junit}" - testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-inline:${versions.mockito}" - testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" - testImplementation 'com.github.cketti:kxml2-extracted-from-android:1.0' + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.inline + testImplementation libs.mockito.kotlin + testImplementation libs.kxml2 } diff --git a/app/core/build.gradle b/app/core/build.gradle index b9f479ea7..9c8ca3079 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -1,6 +1,8 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' -apply plugin: 'org.jetbrains.kotlin.plugin.parcelize' +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.parcelize) +} dependencies { api project(":mail:common") @@ -9,37 +11,37 @@ dependencies { implementation project(':plugins:openpgp-api-lib:openpgp-api') - api "io.insert-koin:koin-android:${versions.koin}" + api libs.koin.android - api "androidx.annotation:annotation:${versions.androidxAnnotation}" + api libs.androidx.annotation - implementation "com.squareup.okio:okio:${versions.okio}" - implementation "commons-io:commons-io:${versions.commonsIo}" - implementation "androidx.core:core-ktx:${versions.androidxCore}" - implementation "androidx.work:work-runtime-ktx:${versions.androidxWorkManager}" - implementation "androidx.fragment:fragment:${versions.androidxFragment}" - implementation "androidx.localbroadcastmanager:localbroadcastmanager:${versions.androidxLocalBroadcastManager}" - 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}" + implementation libs.okio + implementation libs.commons.io + implementation libs.androidx.core.ktx + implementation libs.androidx.work.ktx + implementation libs.androidx.fragment + implementation libs.androidx.localbroadcastmanager + implementation libs.jsoup + implementation libs.moshi + implementation libs.timber + implementation libs.mime4j.core testImplementation project(':mail:testing') testImplementation project(":backend:imap") testImplementation project(":mail:protocols:smtp") testImplementation project(":app:storage") testImplementation project(":app:testing") - testImplementation "org.jetbrains.kotlin:kotlin-test:${versions.kotlin}" - testImplementation "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}" - testImplementation "org.robolectric:robolectric:${versions.robolectric}" - testImplementation "androidx.test:core:${versions.androidxTestCore}" - testImplementation "junit:junit:${versions.junit}" - testImplementation "com.google.truth:truth:${versions.truth}" - 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}" - testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}" + testImplementation libs.kotlin.test + testImplementation libs.kotlin.reflect + testImplementation libs.robolectric + testImplementation libs.androidx.test.core + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.inline + testImplementation libs.mockito.kotlin + testImplementation libs.jdom2 + testImplementation libs.koin.test + testImplementation libs.koin.test.junit4 } android { diff --git a/app/core/src/main/AndroidManifest.xml b/app/core/src/main/AndroidManifest.xml index 60d884212..e4c26a20a 100644 --- a/app/core/src/main/AndroidManifest.xml +++ b/app/core/src/main/AndroidManifest.xml @@ -1,26 +1,7 @@ - + - - - - - - - - - - - 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 d52f49dc8..08cbf947b 100644 --- a/app/core/src/main/java/com/fsck/k9/K9.kt +++ b/app/core/src/main/java/com/fsck/k9/K9.kt @@ -217,6 +217,10 @@ object K9 : EarlyInit { @JvmStatic var isUseBackgroundAsUnreadIndicator = false + @get:Synchronized + @set:Synchronized + var isShowComposeButtonOnMessageList = true + @get:Synchronized @set:Synchronized @JvmStatic @@ -347,6 +351,7 @@ object K9 : EarlyInit { splitViewMode = storage.getEnum("splitViewMode", SplitViewMode.NEVER) isUseBackgroundAsUnreadIndicator = storage.getBoolean("useBackgroundAsUnreadIndicator", false) + isShowComposeButtonOnMessageList = storage.getBoolean("showComposeButtonOnMessageList", true) isThreadedViewEnabled = storage.getBoolean("threadedView", true) fontSizes.load(storage) @@ -413,6 +418,7 @@ object K9 : EarlyInit { editor.putString("lockScreenNotificationVisibility", lockScreenNotificationVisibility.toString()) editor.putBoolean("useBackgroundAsUnreadIndicator", isUseBackgroundAsUnreadIndicator) + editor.putBoolean("showComposeButtonOnMessageList", isShowComposeButtonOnMessageList) editor.putBoolean("threadedView", isThreadedViewEnabled) editor.putEnum("splitViewMode", splitViewMode) editor.putBoolean("colorizeMissingContactPictures", isColorizeMissingContactPictures) 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 81f86dcc0..16944e743 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 @@ -1894,7 +1894,15 @@ public class MessagingController { }); } + public void deleteDraftSkippingTrashFolder(Account account, long messageId) { + deleteDraft(account, messageId, true); + } + public void deleteDraft(Account account, long messageId) { + deleteDraft(account, messageId, false); + } + + private void deleteDraft(Account account, long messageId, boolean skipTrashFolder) { Long folderId = account.getDraftsFolderId(); if (folderId == null) { Timber.w("No Drafts folder configured. Can't delete draft."); @@ -1905,7 +1913,7 @@ public class MessagingController { String messageServerId = messageStore.getMessageServerId(messageId); if (messageServerId != null) { MessageReference messageReference = new MessageReference(account.getUuid(), folderId, messageServerId); - deleteMessage(messageReference); + deleteMessages(Collections.singletonList(messageReference), skipTrashFolder); } } @@ -1913,15 +1921,15 @@ public class MessagingController { actOnMessagesGroupedByAccountAndFolder(messages, (account, messageFolder, accountMessages) -> { suppressMessages(account, accountMessages); putBackground("deleteThreads", null, () -> - deleteThreadsSynchronous(account, messageFolder.getDatabaseId(), accountMessages) + deleteThreadsSynchronous(account, messageFolder.getDatabaseId(), accountMessages, false) ); }); } - private void deleteThreadsSynchronous(Account account, long folderId, List messages) { + private void deleteThreadsSynchronous(Account account, long folderId, List messages, boolean skipTrashFolder) { try { List messagesToDelete = collectMessagesInThreads(account, messages); - deleteMessagesSynchronous(account, folderId, messagesToDelete); + deleteMessagesSynchronous(account, folderId, messagesToDelete, skipTrashFolder); } catch (MessagingException e) { Timber.e(e, "Something went wrong while deleting threads"); } @@ -1946,14 +1954,18 @@ public class MessagingController { } public void deleteMessage(MessageReference message) { - deleteMessages(Collections.singletonList(message)); + deleteMessages(Collections.singletonList(message), false); } public void deleteMessages(List messages) { + deleteMessages(messages, false); + } + + private void deleteMessages(List messages, boolean skipTrashFolder) { actOnMessagesGroupedByAccountAndFolder(messages, (account, messageFolder, accountMessages) -> { suppressMessages(account, accountMessages); putBackground("deleteMessages", null, () -> - deleteMessagesSynchronous(account, messageFolder.getDatabaseId(), accountMessages) + deleteMessagesSynchronous(account, messageFolder.getDatabaseId(), accountMessages, skipTrashFolder) ); }); } @@ -1987,7 +1999,7 @@ public class MessagingController { } - private void deleteMessagesSynchronous(Account account, long folderId, List messages) { + private void deleteMessagesSynchronous(Account account, long folderId, List messages, boolean skipTrashFolder) { try { List localOnlyMessages = new ArrayList<>(); List syncedMessages = new ArrayList<>(); @@ -2012,8 +2024,10 @@ public class MessagingController { Map uidMap = null; Long trashFolderId = account.getTrashFolderId(); + boolean isSpamFolder = account.hasSpamFolder() && account.getSpamFolderId() == folderId; + LocalFolder localTrashFolder = null; - if (!account.hasTrashFolder() || folderId == trashFolderId || + if (skipTrashFolder || !account.hasTrashFolder() || folderId == trashFolderId || isSpamFolder || (backend.getSupportsTrashFolder() && !backend.isDeleteMoveToTrash())) { Timber.d("Not moving deleted messages to local Trash folder. Removing local copies."); diff --git a/app/core/src/main/java/com/fsck/k9/helper/Utility.java b/app/core/src/main/java/com/fsck/k9/helper/Utility.java index 1788f8b3a..5671f68bb 100644 --- a/app/core/src/main/java/com/fsck/k9/helper/Utility.java +++ b/app/core/src/main/java/com/fsck/k9/helper/Utility.java @@ -121,7 +121,7 @@ public class Utility { /** * Extract the 'original' subject value, by ignoring leading * response/forward marker and '[XX]' formatted tags (as many mailing-list - * softwares do). + * software do). * *

* Result is also trimmed. diff --git a/app/core/src/main/java/com/fsck/k9/job/K9WorkerFactory.kt b/app/core/src/main/java/com/fsck/k9/job/K9WorkerFactory.kt index adcb92588..9251fb486 100644 --- a/app/core/src/main/java/com/fsck/k9/job/K9WorkerFactory.kt +++ b/app/core/src/main/java/com/fsck/k9/job/K9WorkerFactory.kt @@ -4,23 +4,16 @@ import android.content.Context import androidx.work.ListenableWorker import androidx.work.WorkerFactory import androidx.work.WorkerParameters -import com.fsck.k9.Preferences -import com.fsck.k9.controller.MessagingController +import org.koin.core.parameter.parametersOf +import org.koin.java.KoinJavaComponent.getKoin -class K9WorkerFactory( - private val messagingController: MessagingController, - private val preferences: Preferences -) : WorkerFactory() { +class K9WorkerFactory : WorkerFactory() { override fun createWorker( appContext: Context, workerClassName: String, workerParameters: WorkerParameters ): ListenableWorker? { - return when (workerClassName) { - MailSyncWorker::class.java.canonicalName -> { - MailSyncWorker(messagingController, preferences, appContext, workerParameters) - } - else -> null - } + val workerClass = Class.forName(workerClassName).kotlin + return getKoin().getOrNull(workerClass) { parametersOf(workerParameters) } } } diff --git a/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt index 4dd6f0e8f..8158ae494 100644 --- a/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt +++ b/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt @@ -1,12 +1,18 @@ package com.fsck.k9.job +import android.content.Context +import androidx.work.WorkManager import androidx.work.WorkerFactory +import androidx.work.WorkerParameters import org.koin.dsl.module val jobModule = module { - single { WorkManagerProvider(get(), get()) } - single { K9WorkerFactory(get(), get()) } - single { get().getWorkManager() } - single { K9JobManager(get(), get(), get()) } + single { WorkManagerConfigurationProvider(workerFactory = get()) } + single { K9WorkerFactory() } + single { WorkManager.getInstance(get()) } + single { K9JobManager(workManager = get(), preferences = get(), mailSyncWorkerManager = get()) } factory { MailSyncWorkerManager(workManager = get(), clock = get()) } + factory { (parameters: WorkerParameters) -> + MailSyncWorker(messagingController = get(), preferences = get(), context = get(), parameters) + } } diff --git a/app/core/src/main/java/com/fsck/k9/job/WorkManagerConfigurationProvider.kt b/app/core/src/main/java/com/fsck/k9/job/WorkManagerConfigurationProvider.kt new file mode 100644 index 000000000..54d973fee --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/job/WorkManagerConfigurationProvider.kt @@ -0,0 +1,12 @@ +package com.fsck.k9.job + +import androidx.work.Configuration +import androidx.work.WorkerFactory + +class WorkManagerConfigurationProvider(private val workerFactory: WorkerFactory) { + fun getConfiguration(): Configuration { + return Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() + } +} diff --git a/app/core/src/main/java/com/fsck/k9/job/WorkManagerProvider.kt b/app/core/src/main/java/com/fsck/k9/job/WorkManagerProvider.kt deleted file mode 100644 index f31fbaaf7..000000000 --- a/app/core/src/main/java/com/fsck/k9/job/WorkManagerProvider.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.fsck.k9.job - -import android.content.Context -import androidx.work.Configuration -import androidx.work.WorkManager -import androidx.work.WorkerFactory - -class WorkManagerProvider(private val context: Context, private val workerFactory: WorkerFactory) { - fun getWorkManager(): WorkManager { - val configuration = Configuration.Builder() - .setWorkerFactory(workerFactory) - .build() - - WorkManager.initialize(context, configuration) - - return WorkManager.getInstance(context) - } -} diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java index 3f846a6c6..01af19471 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -651,9 +651,13 @@ public class LocalStore { if (MimeUtil.ENC_QUOTED_PRINTABLE.equals(encoding)) { return new QuotedPrintableInputStream(rawInputStream) { @Override - public void close() throws IOException { + public void close() { super.close(); - rawInputStream.close(); + try { + rawInputStream.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } } }; } diff --git a/app/core/src/main/java/com/fsck/k9/message/extractors/TextPartFinder.java b/app/core/src/main/java/com/fsck/k9/message/extractors/TextPartFinder.java deleted file mode 100644 index 2168da54f..000000000 --- a/app/core/src/main/java/com/fsck/k9/message/extractors/TextPartFinder.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.fsck.k9.message.extractors; - - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.fsck.k9.mail.Body; -import com.fsck.k9.mail.BodyPart; -import com.fsck.k9.mail.Multipart; -import com.fsck.k9.mail.Part; - -import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType; - - -public class TextPartFinder { - @Nullable - public Part findFirstTextPart(@NonNull Part part) { - String mimeType = part.getMimeType(); - Body body = part.getBody(); - - if (body instanceof Multipart) { - Multipart multipart = (Multipart) body; - if (isSameMimeType(mimeType, "multipart/alternative")) { - return findTextPartInMultipartAlternative(multipart); - } else { - return findTextPartInMultipart(multipart); - } - } else if (isSameMimeType(mimeType, "text/plain") || isSameMimeType(mimeType, "text/html")) { - return part; - } - - return null; - } - - private Part findTextPartInMultipartAlternative(Multipart multipart) { - Part htmlPart = null; - - for (BodyPart bodyPart : multipart.getBodyParts()) { - String mimeType = bodyPart.getMimeType(); - Body body = bodyPart.getBody(); - - if (body instanceof Multipart) { - Part candidatePart = findFirstTextPart(bodyPart); - if (candidatePart != null) { - if (isSameMimeType(candidatePart.getMimeType(), "text/html")) { - htmlPart = candidatePart; - } else { - return candidatePart; - } - } - } else if (isSameMimeType(mimeType, "text/plain")) { - return bodyPart; - } else if (isSameMimeType(mimeType, "text/html") && htmlPart == null) { - htmlPart = bodyPart; - } - } - - if (htmlPart != null) { - return htmlPart; - } - - return null; - } - - private Part findTextPartInMultipart(Multipart multipart) { - for (BodyPart bodyPart : multipart.getBodyParts()) { - String mimeType = bodyPart.getMimeType(); - Body body = bodyPart.getBody(); - - if (body instanceof Multipart) { - Part candidatePart = findFirstTextPart(bodyPart); - if (candidatePart != null) { - return candidatePart; - } - } else if (isSameMimeType(mimeType, "text/plain") || isSameMimeType(mimeType, "text/html")) { - return bodyPart; - } - } - - return null; - } -} diff --git a/app/core/src/main/java/com/fsck/k9/message/extractors/TextPartFinder.kt b/app/core/src/main/java/com/fsck/k9/message/extractors/TextPartFinder.kt new file mode 100644 index 000000000..cca3d79c2 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/message/extractors/TextPartFinder.kt @@ -0,0 +1,68 @@ +package com.fsck.k9.message.extractors + +import com.fsck.k9.mail.MimeType.Companion.toMimeType +import com.fsck.k9.mail.MimeType.Companion.toMimeTypeOrNull +import com.fsck.k9.mail.Multipart +import com.fsck.k9.mail.Part + +private val TEXT_PLAIN = "text/plain".toMimeType() +private val TEXT_HTML = "text/html".toMimeType() +private val MULTIPART_ALTERNATIVE = "multipart/alternative".toMimeType() + +class TextPartFinder { + fun findFirstTextPart(part: Part): Part? { + val mimeType = part.mimeType.toMimeTypeOrNull() + val body = part.body + + return if (body is Multipart) { + if (mimeType == MULTIPART_ALTERNATIVE) { + findTextPartInMultipartAlternative(body) + } else { + findTextPartInMultipart(body) + } + } else if (mimeType == TEXT_PLAIN || mimeType == TEXT_HTML) { + part + } else { + null + } + } + + private fun findTextPartInMultipartAlternative(multipart: Multipart): Part? { + var htmlPart: Part? = null + + for (bodyPart in multipart.bodyParts) { + val mimeType = bodyPart.mimeType.toMimeTypeOrNull() + val body = bodyPart.body + + if (body is Multipart) { + val candidatePart = findFirstTextPart(bodyPart) ?: continue + if (mimeType == TEXT_PLAIN) { + return candidatePart + } + + htmlPart = candidatePart + } else if (mimeType == TEXT_PLAIN) { + return bodyPart + } else if (mimeType == TEXT_HTML && htmlPart == null) { + htmlPart = bodyPart + } + } + + return htmlPart + } + + private fun findTextPartInMultipart(multipart: Multipart): Part? { + for (bodyPart in multipart.bodyParts) { + val mimeType = bodyPart.mimeType.toMimeTypeOrNull() + val body = bodyPart.body + + if (body is Multipart) { + return findFirstTextPart(bodyPart) ?: continue + } else if (mimeType == TEXT_PLAIN || mimeType == TEXT_HTML) { + return bodyPart + } + } + + return null + } +} diff --git a/app/core/src/main/java/com/fsck/k9/message/html/BitcoinUriParser.java b/app/core/src/main/java/com/fsck/k9/message/html/BitcoinUriParser.java deleted file mode 100644 index 632231a00..000000000 --- a/app/core/src/main/java/com/fsck/k9/message/html/BitcoinUriParser.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.fsck.k9.message.html; - - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - - -class BitcoinUriParser implements UriParser { - private static final Pattern BITCOIN_URI_PATTERN = - Pattern.compile("bitcoin:[1-9a-km-zA-HJ-NP-Z]{27,34}(\\?[a-zA-Z0-9$\\-_.+!*'(),%:@&=]*)?"); - - @Nullable - @Override - public UriMatch parseUri(@NotNull CharSequence text, int startPos) { - Matcher matcher = BITCOIN_URI_PATTERN.matcher(text); - - if (!matcher.find(startPos) || matcher.start() != startPos) { - return null; - } - - int startIndex = matcher.start(); - int endIndex = matcher.end(); - CharSequence uri = text.subSequence(startIndex, endIndex); - return new UriMatch(startIndex, endIndex, uri); - } -} diff --git a/app/core/src/main/java/com/fsck/k9/message/html/EthereumUriParser.java b/app/core/src/main/java/com/fsck/k9/message/html/EthereumUriParser.java deleted file mode 100644 index 70f5878a2..000000000 --- a/app/core/src/main/java/com/fsck/k9/message/html/EthereumUriParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.fsck.k9.message.html; - - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - - -/** - * Parses ERC-67 URIs - * https://github.com/ethereum/EIPs/issues/67 - */ -class EthereumUriParser implements UriParser { - private static final Pattern ETHEREUM_URI_PATTERN = - Pattern.compile("ethereum:0x[0-9a-fA-F]*(\\?[a-zA-Z0-9$\\-_.+!*'(),%:@&=]*)?"); - - @Nullable - @Override - public UriMatch parseUri(@NotNull CharSequence text, int startPos) { - Matcher matcher = ETHEREUM_URI_PATTERN.matcher(text); - - if (!matcher.find(startPos) || matcher.start() != startPos) { - return null; - } - - int startIndex = matcher.start(); - int endIndex = matcher.end(); - CharSequence uri = text.subSequence(startIndex, endIndex); - return new UriMatch(startIndex, endIndex, uri); - } -} diff --git a/app/core/src/main/java/com/fsck/k9/message/html/GenericUriParser.kt b/app/core/src/main/java/com/fsck/k9/message/html/GenericUriParser.kt new file mode 100644 index 000000000..14cede4cd --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/message/html/GenericUriParser.kt @@ -0,0 +1,34 @@ +package com.fsck.k9.message.html + +import java.util.regex.Pattern + +/** + * Matches the URI generic syntax. + * + * See [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986). + */ +class GenericUriParser : UriParser { + override fun parseUri(text: CharSequence, startPos: Int): UriMatch? { + val matcher = PATTERN.matcher(text) + if (!matcher.find(startPos) || matcher.start() != startPos) return null + + val startIndex = matcher.start() + val endIndex = matcher.end() + val uri = text.subSequence(startIndex, endIndex) + + return UriMatch(startIndex, endIndex, uri) + } + + companion object { + private const val SCHEME = "[a-zA-Z][a-zA-Z0-9+.\\-]*" + private const val AUTHORITY = "[a-zA-Z0-9\\-._~%!\$&'()*+,;=:\\[\\]@]*" + private const val PATH = "[a-zA-Z0-9\\-._~%!\$&'()*+,;=:@/]*" + private const val QUERY = "[a-zA-Z0-9\\-._~%!\$&'()*+,;=:@/?]*" + private const val FRAGMENT = "[a-zA-Z0-9\\-._~%!\$&'()*+,;=:@/?]*" + + // This regular expression matches more than allowed by the generic URI syntax. So we might end up linkifying + // text that is not a proper URI. We leave apps actually handling the URI when the user clicks on such a link + // to deal with this case. + private val PATTERN = Pattern.compile("$SCHEME:(?://$AUTHORITY)?(?:$PATH)?(?:\\?$QUERY)?(?:#$FRAGMENT)?") + } +} diff --git a/app/core/src/main/java/com/fsck/k9/message/html/UriMatcher.kt b/app/core/src/main/java/com/fsck/k9/message/html/UriMatcher.kt index 5dea02560..44329acfd 100644 --- a/app/core/src/main/java/com/fsck/k9/message/html/UriMatcher.kt +++ b/app/core/src/main/java/com/fsck/k9/message/html/UriMatcher.kt @@ -3,12 +3,14 @@ package com.fsck.k9.message.html object UriMatcher { private val SUPPORTED_URIS = run { val httpUriParser = HttpUriParser() + val genericUriParser = GenericUriParser() mapOf( - "ethereum:" to EthereumUriParser(), - "bitcoin:" to BitcoinUriParser(), "http:" to httpUriParser, "https:" to httpUriParser, - "rtsp:" to httpUriParser + "mailto:" to genericUriParser, + "matrix:" to genericUriParser, + "rtsp:" to httpUriParser, + "xmpp:" to genericUriParser, ) } 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 af8a22247..cb4bfeb8e 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 @@ -271,6 +271,9 @@ public class GeneralSettingsDescriptions { s.put("swipeLeftAction", Settings.versions( new V(83, new EnumSetting<>(SwipeAction.class, SwipeAction.ToggleRead)) )); + s.put("showComposeButtonOnMessageList", Settings.versions( + new V(85, new BooleanSetting(true)) + )); SETTINGS = Collections.unmodifiableMap(s); 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 85ffdd1ca..7f1274952 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 = 84; + public static final int VERSION = 85; static Map validate(int version, Map> settings, Map importedSettings, boolean useDefaultValues) { diff --git a/app/core/src/main/java/com/fsck/k9/search/LocalSearch.java b/app/core/src/main/java/com/fsck/k9/search/LocalSearch.java index 52f23ef4a..e538a9723 100644 --- a/app/core/src/main/java/com/fsck/k9/search/LocalSearch.java +++ b/app/core/src/main/java/com/fsck/k9/search/LocalSearch.java @@ -241,7 +241,7 @@ public class LocalSearch implements SearchSpecification { } /////////////////////////////////////////////////////////////// - // Public accesor methods + // Public accessor methods /////////////////////////////////////////////////////////////// /** * TODO THIS HAS TO GO!!!! diff --git a/app/core/src/test/java/com/fsck/k9/TestApp.kt b/app/core/src/test/java/com/fsck/k9/TestApp.kt index b365bd4dd..7f6f0f886 100644 --- a/app/core/src/test/java/com/fsck/k9/TestApp.kt +++ b/app/core/src/test/java/com/fsck/k9/TestApp.kt @@ -1,6 +1,7 @@ package com.fsck.k9 import android.app.Application +import androidx.work.WorkManager import com.fsck.k9.backend.BackendManager import com.fsck.k9.controller.ControllerExtension import com.fsck.k9.crypto.EncryptionExtractor @@ -36,4 +37,5 @@ val testModule = module { single { mock() } single { mock() } single(named("controllerExtensions")) { emptyList() } + single { mock() } } diff --git a/app/core/src/test/java/com/fsck/k9/message/extractors/TextPartFinderTest.java b/app/core/src/test/java/com/fsck/k9/message/extractors/TextPartFinderTest.java deleted file mode 100644 index 30f6b2d19..000000000 --- a/app/core/src/test/java/com/fsck/k9/message/extractors/TextPartFinderTest.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.fsck.k9.message.extractors; - - -import com.fsck.k9.mail.BodyPart; -import com.fsck.k9.mail.Part; -import org.junit.Before; -import org.junit.Test; - -import static com.fsck.k9.message.MessageCreationHelper.createEmptyPart; -import static com.fsck.k9.message.MessageCreationHelper.createMultipart; -import static com.fsck.k9.message.MessageCreationHelper.createPart; -import static com.fsck.k9.message.MessageCreationHelper.createTextPart; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - - -public class TextPartFinderTest { - private TextPartFinder textPartFinder; - - - @Before - public void setUp() throws Exception { - textPartFinder = new TextPartFinder(); - } - - @Test - public void findFirstTextPart_withTextPlainPart() throws Exception { - Part part = createTextPart("text/plain"); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(part, result); - } - - @Test - public void findFirstTextPart_withTextHtmlPart() throws Exception { - Part part = createTextPart("text/html"); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(part, result); - } - - @Test - public void findFirstTextPart_withoutTextPart() throws Exception { - Part part = createPart("image/jpeg"); - - Part result = textPartFinder.findFirstTextPart(part); - - assertNull(result); - } - - @Test - public void findFirstTextPart_withMultipartAlternative() throws Exception { - BodyPart expected = createTextPart("text/plain"); - Part part = createMultipart("multipart/alternative", expected, createTextPart("text/html")); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } - - @Test - public void findFirstTextPart_withMultipartAlternativeHtmlPartFirst() throws Exception { - BodyPart expected = createTextPart("text/plain"); - Part part = createMultipart("multipart/alternative", createTextPart("text/html"), expected); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } - - @Test - public void findFirstTextPart_withMultipartAlternativeContainingOnlyTextHtmlPart() throws Exception { - BodyPart expected = createTextPart("text/html"); - Part part = createMultipart("multipart/alternative", - createPart("image/gif"), - expected, - createTextPart("text/html")); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } - - @Test - public void findFirstTextPart_withMultipartAlternativeNotContainingTextPart() throws Exception { - Part part = createMultipart("multipart/alternative", - createPart("image/gif"), - createPart("application/pdf")); - - Part result = textPartFinder.findFirstTextPart(part); - - assertNull(result); - } - - @Test - public void findFirstTextPart_withMultipartAlternativeContainingMultipartRelatedContainingTextPlain() - throws Exception { - BodyPart expected = createTextPart("text/plain"); - Part part = createMultipart("multipart/alternative", - createMultipart("multipart/related", expected, createPart("image/jpeg")), - createTextPart("text/html")); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } - - @Test - public void findFirstTextPart_withMultipartAlternativeContainingMultipartRelatedContainingTextHtmlFirst() - throws Exception { - BodyPart expected = createTextPart("text/plain"); - Part part = createMultipart("multipart/alternative", - createMultipart("multipart/related", createTextPart("text/html"), createPart("image/jpeg")), - expected); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } - - @Test - public void findFirstTextPart_withMultipartMixedContainingTextPlain() throws Exception { - BodyPart expected = createTextPart("text/plain"); - Part part = createMultipart("multipart/mixed", createPart("image/jpeg"), expected); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } - - @Test - public void findFirstTextPart_withMultipartMixedContainingTextHtmlFirst() throws Exception { - BodyPart expected = createTextPart("text/html"); - Part part = createMultipart("multipart/mixed", expected, createTextPart("text/plain")); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } - - @Test - public void findFirstTextPart_withMultipartMixedNotContainingTextPart() throws Exception { - Part part = createMultipart("multipart/mixed", createPart("image/jpeg"), createPart("image/gif")); - - Part result = textPartFinder.findFirstTextPart(part); - - assertNull(result); - } - - @Test - public void findFirstTextPart_withMultipartMixedContainingMultipartAlternative() throws Exception { - BodyPart expected = createTextPart("text/plain"); - Part part = createMultipart("multipart/mixed", - createPart("image/jpeg"), - createMultipart("multipart/alternative", expected, createTextPart("text/html")), - createTextPart("text/plain")); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } - - @Test - public void findFirstTextPart_withMultipartMixedContainingMultipartAlternativeWithTextPlainPartLast() - throws Exception { - BodyPart expected = createTextPart("text/plain"); - Part part = createMultipart("multipart/mixed", - createMultipart("multipart/alternative", createTextPart("text/html"), expected)); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } - - @Test - public void findFirstTextPart_withMultipartAlternativeContainingEmptyTextPlainPart() - throws Exception { - BodyPart expected = createEmptyPart("text/plain"); - Part part = createMultipart("multipart/alternative", expected, createTextPart("text/html")); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } - - @Test - public void findFirstTextPart_withMultipartMixedContainingEmptyTextHtmlPart() - throws Exception { - BodyPart expected = createEmptyPart("text/html"); - Part part = createMultipart("multipart/mixed", expected, createTextPart("text/plain")); - - Part result = textPartFinder.findFirstTextPart(part); - - assertEquals(expected, result); - } -} diff --git a/app/core/src/test/java/com/fsck/k9/message/extractors/TextPartFinderTest.kt b/app/core/src/test/java/com/fsck/k9/message/extractors/TextPartFinderTest.kt new file mode 100644 index 000000000..4f7f32c09 --- /dev/null +++ b/app/core/src/test/java/com/fsck/k9/message/extractors/TextPartFinderTest.kt @@ -0,0 +1,254 @@ +package com.fsck.k9.message.extractors + +import com.fsck.k9.message.MessageCreationHelper.createEmptyPart +import com.fsck.k9.message.MessageCreationHelper.createMultipart +import com.fsck.k9.message.MessageCreationHelper.createPart +import com.fsck.k9.message.MessageCreationHelper.createTextPart +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class TextPartFinderTest { + private val textPartFinder = TextPartFinder() + + @Test + fun `text_plain part`() { + val part = createTextPart("text/plain") + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(part) + } + + @Test + fun `text_html part`() { + val part = createTextPart("text/html") + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(part) + } + + @Test + fun `without text part`() { + val part = createPart("image/jpeg") + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isNull() + } + + @Test + fun `multipart_alternative text_plain and text_html`() { + val expected = createTextPart("text/plain") + val part = createMultipart( + "multipart/alternative", + expected, + createTextPart("text/html") + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_alternative containing text_html and text_plain`() { + val expected = createTextPart("text/plain") + val part = createMultipart( + "multipart/alternative", + createTextPart("text/html"), + expected + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_alternative containing multiple text_html parts`() { + val expected = createTextPart("text/html") + val part = createMultipart( + "multipart/alternative", + createPart("image/gif"), + expected, + createTextPart("text/html") + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_alternative not containing any text parts`() { + val part = createMultipart( + "multipart/alternative", + createPart("image/gif"), + createPart("application/pdf") + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isNull() + } + + @Test + fun `multipart_alternative containing multipart_related containing text_plain`() { + val expected = createTextPart("text/plain") + val part = createMultipart( + "multipart/alternative", + createMultipart( + "multipart/related", + expected, + createPart("image/jpeg") + ), + createTextPart("text/html") + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_alternative containing multipart_related and text_plain`() { + val expected = createTextPart("text/plain") + val part = createMultipart( + "multipart/alternative", + createMultipart( + "multipart/related", + createTextPart("text/html"), + createPart("image/jpeg") + ), + expected + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_mixed containing text_plain`() { + val expected = createTextPart("text/plain") + val part = createMultipart( + "multipart/mixed", + createPart("image/jpeg"), + expected + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_mixed containing text_html and text_plain`() { + val expected = createTextPart("text/html") + val part = createMultipart( + "multipart/mixed", + expected, + createTextPart("text/plain") + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_mixed not containing any text parts`() { + val part = createMultipart( + "multipart/mixed", + createPart("image/jpeg"), + createPart("image/gif") + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isNull() + } + + @Test + fun `multipart_mixed containing multipart_alternative containing text_plain and text_html`() { + val expected = createTextPart("text/plain") + val part = createMultipart( + "multipart/mixed", + createPart("image/jpeg"), + createMultipart( + "multipart/alternative", + expected, + createTextPart("text/html") + ), + createTextPart("text/plain") + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_mixed containing multipart_alternative containing text_html and text_plain`() { + val expected = createTextPart("text/plain") + val part = createMultipart( + "multipart/mixed", + createMultipart( + "multipart/alternative", + createTextPart("text/html"), + expected + ) + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_alternative containing empty text_plain and text_html`() { + val expected = createEmptyPart("text/plain") + val part = createMultipart( + "multipart/alternative", + expected, + createTextPart("text/html") + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_mixed containing empty text_html and text_plain`() { + val expected = createEmptyPart("text/html") + val part = createMultipart( + "multipart/mixed", + expected, + createTextPart("text/plain") + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `multipart_mixed containing multipart_alternative and text_plain`() { + val expected = createEmptyPart("text/plain") + val part = createMultipart( + "multipart/mixed", + createMultipart( + "multipart/alternative", + createPart("image/jpeg"), + createPart("image/png") + ), + expected + ) + + val result = textPartFinder.findFirstTextPart(part) + + assertThat(result).isEqualTo(expected) + } +} diff --git a/app/core/src/test/java/com/fsck/k9/message/html/BitcoinUriParserTest.java b/app/core/src/test/java/com/fsck/k9/message/html/BitcoinUriParserTest.java deleted file mode 100644 index 0478024ce..000000000 --- a/app/core/src/test/java/com/fsck/k9/message/html/BitcoinUriParserTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.fsck.k9.message.html; - - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - - -public class BitcoinUriParserTest { - BitcoinUriParser parser = new BitcoinUriParser(); - - - @Test - public void basicBitcoinUri() throws Exception { - assertValidUri("bitcoin:19W6QZkx8SYPG7BBCS7odmWGRxqRph5jFU"); - } - - @Test - public void bitcoinUriWithAmount() throws Exception { - assertValidUri("bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2"); - } - - @Test - public void bitcoinUriWithQueryParameters() throws Exception { - assertValidUri("bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2" + - "&message=Payment&label=Satoshi&extra=other-param"); - } - - @Test - public void uriInMiddleOfInput() throws Exception { - String prefix = "prefix "; - String uri = "bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2"; - String text = prefix + uri; - - UriMatch uriMatch = parser.parseUri(text, prefix.length()); - - assertUriMatch(uri, uriMatch, prefix.length()); - } - - @Test - public void invalidScheme() throws Exception { - assertInvalidUri("bitcion:19W6QZkx8SYPG7BBCS7odmWGRxqRph5jFU"); - } - - @Test - public void invalidAddress() throws Exception { - assertInvalidUri("bitcoin:[invalid]"); - } - - - private void assertValidUri(String uri) { - UriMatch uriMatch = parser.parseUri(uri, 0); - assertUriMatch(uri, uriMatch); - } - - private void assertUriMatch(String uri, UriMatch uriMatch) { - assertUriMatch(uri, uriMatch, 0); - } - - private void assertUriMatch(String uri, UriMatch uriMatch, int offset) { - assertNotNull(uriMatch); - assertEquals(offset, uriMatch.getStartIndex()); - assertEquals(uri.length() + offset, uriMatch.getEndIndex()); - assertEquals(uri, uriMatch.getUri().toString()); - } - - private void assertInvalidUri(String text) { - UriMatch uriMatch = parser.parseUri(text, 0); - assertNull(uriMatch); - } -} diff --git a/app/core/src/test/java/com/fsck/k9/message/html/EthereumUriParserTest.java b/app/core/src/test/java/com/fsck/k9/message/html/EthereumUriParserTest.java deleted file mode 100644 index 9927f329c..000000000 --- a/app/core/src/test/java/com/fsck/k9/message/html/EthereumUriParserTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.fsck.k9.message.html; - - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - - -public class EthereumUriParserTest { - EthereumUriParser parser = new EthereumUriParser(); - - - @Test - public void basicEthereumUri() throws Exception { - assertValidUri("ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4"); - } - - @Test - public void ethereumUriWithValue() throws Exception { - assertValidUri("ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4?value=42"); - } - - @Test - public void ethereumUriWithQueryParameters() throws Exception { - assertValidUri("ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4?value=42" + - "&gas=100000&bytecode=0xa9059cbb0000000000000000000000000000000dead"); - } - - @Test - public void uriInMiddleOfInput() throws Exception { - String prefix = "prefix "; - String uri = "ethereum:0xfdf1210fc262c73d0436236a0e07be419babbbc4?value=42"; - String text = prefix + uri; - - UriMatch uriMatch = parser.parseUri(text, prefix.length()); - - assertUriMatch(uri, uriMatch, prefix.length()); - } - - @Test - public void invalidScheme() throws Exception { - assertInvalidUri("ethereMU:0xfdf1210fc262c73d0436236a0e07be419babbbc4"); - } - - @Test - public void invalidAddress() throws Exception { - assertInvalidUri("ethereum:[invalid]"); - } - - - private void assertValidUri(String uri) { - UriMatch uriMatch = parser.parseUri(uri, 0); - assertUriMatch(uri, uriMatch); - } - - private void assertUriMatch(String uri, UriMatch uriMatch) { - assertUriMatch(uri, uriMatch, 0); - } - - private void assertUriMatch(String uri, UriMatch uriMatch, int offset) { - assertNotNull(uriMatch); - assertEquals(offset, uriMatch.getStartIndex()); - assertEquals(uri.length() + offset, uriMatch.getEndIndex()); - assertEquals(uri, uriMatch.getUri().toString()); - } - - private void assertInvalidUri(String text) { - UriMatch uriMatch = parser.parseUri(text, 0); - assertNull(uriMatch); - } -} diff --git a/app/core/src/test/java/com/fsck/k9/message/html/GenericUriParserTest.kt b/app/core/src/test/java/com/fsck/k9/message/html/GenericUriParserTest.kt new file mode 100644 index 000000000..4dc0b1af1 --- /dev/null +++ b/app/core/src/test/java/com/fsck/k9/message/html/GenericUriParserTest.kt @@ -0,0 +1,71 @@ +package com.fsck.k9.message.html + +import com.google.common.truth.Truth.assertThat +import kotlin.test.assertNotNull +import org.junit.Test + +class GenericUriParserTest { + private val parser = GenericUriParser() + + @Test + fun `mailto URIs`() { + // Examples from RFC 6068 + assertUriValid("mailto:chris@example.com") + assertUriValid("mailto:infobot@example.com?subject=current-issue") + assertUriValid("mailto:infobot@example.com?body=send%20current-issue") + assertUriValid("mailto:infobot@example.com?body=send%20current-issue%0D%0Asend%20index") + assertUriValid("mailto:list@example.org?In-Reply-To=%3C3469A91.D10AF4C@example.com%3E") + assertUriValid("mailto:majordomo@example.com?body=subscribe%20bamboo-l") + assertUriValid("mailto:joe@example.com?cc=bob@example.com&body=hello") + assertUriValid("mailto:gorby%25kremvax@example.com") + assertUriValid("mailto:unlikely%3Faddress@example.com?blat=foop") + assertUriValid("mailto:%22not%40me%22@example.org") + assertUriValid("mailto:%22oh%5C%5Cno%22@example.org") + assertUriValid("mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org") + assertUriValid("mailto:user@example.org?subject=caf%C3%A9") + assertUriValid("mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D") + assertUriValid("mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D") + assertUriValid("mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9") + assertUriValid("mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO") + } + + @Test + fun `XMPP URIs`() { + // Examples from RFC 5122 + assertUriValid("xmpp:node@example.com") + assertUriValid("xmpp://guest@example.com") + assertUriValid("xmpp://guest@example.com/support@example.com?message") + assertUriValid("xmpp:support@example.com?message") + assertUriValid("xmpp:example-node@example.com/some-resource") + assertUriValid("xmpp:example.com") + assertUriValid("xmpp:example-node@example.com?message") + assertUriValid("xmpp:example-node@example.com?message;subject=Hello%20World") + assertUriValid("xmpp:nasty!%23\$%25()*+,-.;=%3F%5B%5C%5D%5E_%60%7B%7C%7D~node@example.com") + assertUriValid("xmpp:node@example.com/repulsive%20!%23%22\$%25&'()*+,-.%2F:;%3C=%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~resource") + assertUriValid("xmpp:ji%C5%99i@%C4%8Dechy.example/v%20Praze") + } + + @Test + fun `matrix URIs`() { + // Examples from MSC 2312 + assertUriValid("matrix:r/someroom:example.org") + assertUriValid("matrix:u/me:example.org") + assertUriValid("matrix:r/someroom:example.org/e/Arbitrary_Event_Id") + assertUriValid("matrix:u/her:example.org") + assertUriValid("matrix:u/her:example.org?action=chat") + assertUriValid("matrix:roomid/rid:example.org") + assertUriValid("matrix:r/us:example.org") + assertUriValid("matrix:roomid/rid:example.org?action=join&via=example2.org") + assertUriValid("matrix:r/us:example.org?action=join") + assertUriValid("matrix:r/us:example.org/e/lol823y4bcp3qo4") + assertUriValid("matrix:roomid/rid:example.org/event/lol823y4bcp3qo4?via=example2.org") + } + + private fun assertUriValid(input: String) { + val result = parser.parseUri(input, 0) + + assertNotNull(result) { uriMatch -> + assertThat(uriMatch.uri).isEqualTo(input) + } + } +} diff --git a/app/crypto-openpgp/build.gradle b/app/crypto-openpgp/build.gradle index 671aa6439..ad360744f 100644 --- a/app/crypto-openpgp/build.gradle +++ b/app/crypto-openpgp/build.gradle @@ -1,11 +1,13 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} dependencies { implementation project(":app:core") - testImplementation "junit:junit:${versions.junit}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" + testImplementation libs.junit + testImplementation libs.mockito.core } android { diff --git a/app/crypto-openpgp/src/test/java/com/fsck/k9/crypto/openpgp/EncryptionDetectorTest.java b/app/crypto-openpgp/src/test/java/com/fsck/k9/crypto/openpgp/EncryptionDetectorTest.java index 494ac3194..992b2d169 100644 --- a/app/crypto-openpgp/src/test/java/com/fsck/k9/crypto/openpgp/EncryptionDetectorTest.java +++ b/app/crypto-openpgp/src/test/java/com/fsck/k9/crypto/openpgp/EncryptionDetectorTest.java @@ -12,23 +12,18 @@ import static com.fsck.k9.crypto.openpgp.MessageCreationHelper.createPart; import static com.fsck.k9.crypto.openpgp.MessageCreationHelper.createTextMessage; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class EncryptionDetectorTest { private static final String CRLF = "\r\n"; - private TextPartFinder textPartFinder; private EncryptionDetector encryptionDetector; @Before public void setUp() { - textPartFinder = mock(TextPartFinder.class); - - encryptionDetector = new EncryptionDetector(textPartFinder); + encryptionDetector = new EncryptionDetector(new TextPartFinder()); } @Test @@ -75,7 +70,6 @@ public class EncryptionDetectorTest { "-----BEGIN PGP MESSAGE-----" + CRLF + "some encrypted stuff here" + CRLF + "-----END PGP MESSAGE-----"); - when(textPartFinder.findFirstTextPart(message)).thenReturn(message); boolean encrypted = encryptionDetector.isEncrypted(message); @@ -90,7 +84,6 @@ public class EncryptionDetectorTest { "some encrypted stuff here" + CRLF + "-----END PGP MESSAGE-----" + CRLF + "epilogue"); - when(textPartFinder.findFirstTextPart(message)).thenReturn(message); boolean encrypted = encryptionDetector.isEncrypted(message); @@ -108,7 +101,6 @@ public class EncryptionDetectorTest { CRLF + "-- " + CRLF + "my signature"); - when(textPartFinder.findFirstTextPart(message)).thenReturn(message); boolean encrypted = encryptionDetector.isEncrypted(message); diff --git a/app/html-cleaner/build.gradle b/app/html-cleaner/build.gradle index e9d5a13e0..6e423bf30 100644 --- a/app/html-cleaner/build.gradle +++ b/app/html-cleaner/build.gradle @@ -1,10 +1,12 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} dependencies { - implementation "org.jsoup:jsoup:${versions.jsoup}" + implementation libs.jsoup - testImplementation "junit:junit:${versions.junit}" - testImplementation "com.google.truth:truth:${versions.truth}" + testImplementation libs.junit + testImplementation libs.truth } diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index cbd49fe57..cde74c44d 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -1,11 +1,15 @@ -apply plugin: 'com.android.application' -apply plugin: 'org.jetbrains.kotlin.android' +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} if (rootProject.testCoverage) { apply plugin: 'jacoco' } dependencies { + coreLibraryDesugaring libs.desugar + implementation project(":app:ui:legacy") implementation project(":app:ui:message-list-widget") implementation project(":app:core") @@ -16,29 +20,30 @@ dependencies { implementation project(":backend:webdav") debugImplementation project(":backend:demo") - implementation "androidx.appcompat:appcompat:${versions.androidxAppCompat}" - implementation "androidx.core:core-ktx:${versions.androidxCore}" - implementation "com.takisoft.preferencex:preferencex:${versions.preferencesFix}" - implementation "com.jakewharton.timber:timber:${versions.timber}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}" + implementation libs.androidx.appcompat + implementation libs.androidx.core.ktx + implementation libs.androidx.work.ktx + implementation libs.preferencex + implementation libs.timber + implementation libs.kotlinx.coroutines.core - implementation "com.github.bumptech.glide:glide:${versions.glide}" - annotationProcessor "com.github.bumptech.glide:compiler:${versions.glide}" + implementation libs.glide + annotationProcessor libs.glide.compiler if (project.hasProperty('k9mail.enableLeakCanary') && project.property('k9mail.enableLeakCanary') == "true") { - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' + debugImplementation libs.leakcanary.android } // 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-inline:${versions.mockito}" - testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" - testImplementation "io.insert-koin:koin-test:${versions.koin}" - testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}" + testImplementation libs.robolectric + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.inline + testImplementation libs.mockito.kotlin + testImplementation libs.koin.test + testImplementation libs.koin.test.junit4 } android { @@ -48,8 +53,8 @@ android { applicationId "com.fsck.k9" testApplicationId "com.fsck.k9.tests" - versionCode 35000 - versionName '6.500-SNAPSHOT' + versionCode 35002 + versionName '6.503-SNAPSHOT' // Keep in sync with the resource string array 'supported_languages' resConfigs "in", "br", "ca", "cs", "cy", "da", "de", "et", "en", "en_GB", "es", "eo", "eu", "fr", "gd", "gl", @@ -66,6 +71,10 @@ android { release } + compileOptions { + coreLibraryDesugaringEnabled true + } + buildTypes { release { if (project.hasProperty('storeFile')) { @@ -100,24 +109,32 @@ android { } } - lintOptions { + lint { checkDependencies true } 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 'META-INF/README.md' - exclude 'META-INF/CHANGES' - exclude 'LICENSE.txt' - exclude 'META-INF/*.kotlin_module' - exclude 'META-INF/*.version' - exclude 'kotlin/**' - exclude 'DebugProbesKt.bin' + jniLibs { + excludes += ['kotlin/**'] + } + + resources { + excludes += [ + 'META-INF/DEPENDENCIES', + 'META-INF/LICENSE', + 'META-INF/LICENSE.txt', + 'META-INF/NOTICE', + 'META-INF/NOTICE.txt', + 'META-INF/README', + 'META-INF/README.md', + 'META-INF/CHANGES', + 'LICENSE.txt', + 'META-INF/*.kotlin_module', + 'META-INF/*.version', + 'kotlin/**', + 'DebugProbesKt.bin' + ] + } } dependenciesInfo { diff --git a/app/k9mail/src/main/AndroidManifest.xml b/app/k9mail/src/main/AndroidManifest.xml index 4a246df16..9b4ccadc6 100644 --- a/app/k9mail/src/main/AndroidManifest.xml +++ b/app/k9mail/src/main/AndroidManifest.xml @@ -175,6 +175,9 @@ + + @@ -287,10 +294,12 @@ android:name=".activity.setup.OAuthFlowActivity" android:label="@string/account_setup_basics_title" /> + @@ -300,10 +309,12 @@ android:resource="@xml/unread_widget_info"/> + @@ -313,6 +324,7 @@ android:resource="@xml/message_list_widget_info" /> + + android:name=".notification.NotificationActionService"/> + + + + + + + diff --git a/app/k9mail/src/main/java/com/fsck/k9/App.kt b/app/k9mail/src/main/java/com/fsck/k9/App.kt index 907bd8a63..7f87dcfe2 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/App.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/App.kt @@ -4,12 +4,16 @@ import android.app.Application import android.content.res.Configuration import android.content.res.Resources import app.k9mail.ui.widget.list.MessageListWidgetManager +import com.fsck.k9.activity.LauncherShortcuts import com.fsck.k9.activity.MessageCompose import com.fsck.k9.controller.MessagingController +import com.fsck.k9.job.WorkManagerConfigurationProvider import com.fsck.k9.notification.NotificationChannelManager +import com.fsck.k9.provider.UnreadWidgetProvider import com.fsck.k9.ui.base.AppLanguageManager import com.fsck.k9.ui.base.ThemeManager import com.fsck.k9.ui.base.extensions.currentLocale +import com.fsck.k9.widget.list.MessageListWidgetProvider import java.util.Locale import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -21,14 +25,17 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.plus import org.koin.android.ext.android.inject import timber.log.Timber +import androidx.work.Configuration as WorkManagerConfiguration -class App : Application() { +class App : Application(), WorkManagerConfiguration.Provider { private val messagingController: MessagingController by inject() private val messagingListenerProvider: MessagingListenerProvider by inject() private val themeManager: ThemeManager by inject() private val appLanguageManager: AppLanguageManager by inject() private val notificationChannelManager: NotificationChannelManager by inject() private val messageListWidgetManager: MessageListWidgetManager by inject() + private val workManagerConfigurationProvider: WorkManagerConfigurationProvider by inject() + private val appCoroutineScope: CoroutineScope = GlobalScope + Dispatchers.Main private var appLanguageManagerInitialized = false @@ -121,9 +128,18 @@ class App : Application() { .launchIn(appCoroutineScope) } + override fun getWorkManagerConfiguration(): WorkManagerConfiguration { + return workManagerConfigurationProvider.getConfiguration() + } + companion object { val appConfig = AppConfig( - componentsToDisable = listOf(MessageCompose::class.java) + componentsToDisable = listOf( + MessageCompose::class.java, + LauncherShortcuts::class.java, + UnreadWidgetProvider::class.java, + MessageListWidgetProvider::class.java, + ) ) } } diff --git a/app/k9mail/src/main/res/values-v31/manifest_values.xml b/app/k9mail/src/main/res/values-v31/manifest_values.xml new file mode 100644 index 000000000..f12344e77 --- /dev/null +++ b/app/k9mail/src/main/res/values-v31/manifest_values.xml @@ -0,0 +1,5 @@ + + + + false + diff --git a/app/k9mail/src/main/res/values/manifest_values.xml b/app/k9mail/src/main/res/values/manifest_values.xml new file mode 100644 index 000000000..034aae205 --- /dev/null +++ b/app/k9mail/src/main/res/values/manifest_values.xml @@ -0,0 +1,9 @@ + + + + true + diff --git a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt index f9da2d72c..09f3508db 100644 --- a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt +++ b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt @@ -1,6 +1,8 @@ package com.fsck.k9 import androidx.lifecycle.LifecycleOwner +import androidx.work.WorkerParameters +import com.fsck.k9.job.MailSyncWorker import com.fsck.k9.ui.changelog.ChangeLogMode import com.fsck.k9.ui.changelog.ChangelogViewModel import com.fsck.k9.ui.endtoend.AutocryptKeyTransferActivity @@ -41,6 +43,7 @@ class DependencyInjectionTest : AutoCloseKoinTest() { withParameter { RuntimeEnvironment.getApplication() } withParameter { RuntimeEnvironment.getApplication() } withParameter { ChangeLogMode.CHANGE_LOG } + withParameter { mock() } } } } diff --git a/app/storage/build.gradle b/app/storage/build.gradle index 7f270f394..d5a8b51c1 100644 --- a/app/storage/build.gradle +++ b/app/storage/build.gradle @@ -1,26 +1,28 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} dependencies { - api "io.insert-koin:koin-core:${versions.koin}" + api libs.koin.core implementation project(":app:core") - implementation "androidx.core:core-ktx:${versions.androidxCore}" - implementation "com.jakewharton.timber:timber:${versions.timber}" - implementation "org.apache.james:apache-mime4j-core:${versions.mime4j}" - implementation "commons-io:commons-io:${versions.commonsIo}" - implementation "com.squareup.moshi:moshi:${versions.moshi}" + implementation libs.androidx.core.ktx + implementation libs.timber + implementation libs.mime4j.core + implementation libs.commons.io + implementation libs.moshi testImplementation project(':mail:testing') testImplementation project(':app:testing') - testImplementation "org.robolectric:robolectric:${versions.robolectric}" - testImplementation "junit:junit:${versions.junit}" - testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-inline:${versions.mockito}" - testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" - testImplementation "io.insert-koin:koin-test:${versions.koin}" - testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}" - testImplementation "commons-io:commons-io:${versions.commonsIo}" + testImplementation libs.robolectric + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.inline + testImplementation libs.mockito.kotlin + testImplementation libs.koin.test + testImplementation libs.koin.test.junit4 + testImplementation libs.commons.io } android { diff --git a/app/testing/build.gradle b/app/testing/build.gradle index 181203c2a..51ee7fd26 100644 --- a/app/testing/build.gradle +++ b/app/testing/build.gradle @@ -1,14 +1,16 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} dependencies { implementation project(':app:core') - api "junit:junit:${versions.junit}" - api "org.robolectric:robolectric:${versions.robolectric}" - api "io.insert-koin:koin-core:${versions.koin}" - api "org.mockito:mockito-core:${versions.mockito}" - api "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" + api libs.junit + api libs.robolectric + api libs.koin.core + api libs.mockito.core + api libs.mockito.kotlin } android { diff --git a/app/ui/base/build.gradle b/app/ui/base/build.gradle index da4e97904..f93989972 100644 --- a/app/ui/base/build.gradle +++ b/app/ui/base/build.gradle @@ -1,20 +1,22 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} dependencies { implementation project(":app:core") - api "androidx.appcompat:appcompat:${versions.androidxAppCompat}" - api "androidx.activity:activity:${versions.androidxActivity}" - api "com.google.android.material:material:${versions.materialComponents}" - api "androidx.navigation:navigation-fragment:${versions.androidxNavigation}" - api "androidx.navigation:navigation-ui:${versions.androidxNavigation}" - api "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}" + api libs.androidx.appcompat + api libs.androidx.activity + api libs.android.material + api libs.androidx.navigation.fragment + api libs.androidx.navigation.ui + api libs.androidx.lifecycle.livedata.ktx - implementation "androidx.core:core-ktx:${versions.androidxCore}" - implementation "androidx.biometric:biometric:${versions.androidxBiometric}" - implementation "com.jakewharton.timber:timber:${versions.timber}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}" + implementation libs.androidx.core.ktx + implementation libs.androidx.biometric + implementation libs.timber + implementation libs.kotlinx.coroutines.core } android { diff --git a/app/ui/base/src/main/AndroidManifest.xml b/app/ui/base/src/main/AndroidManifest.xml index 495700ab6..7e2fb780d 100644 --- a/app/ui/base/src/main/AndroidManifest.xml +++ b/app/ui/base/src/main/AndroidManifest.xml @@ -3,6 +3,9 @@ + if (messageViewContainerFragment == null) { @@ -601,7 +601,7 @@ open class MessageList : openFolderTransaction!!.commit() openFolderTransaction = null - messageListFragment!!.isActive = true + messageListFragment!!.setFullyActive() onMessageListDisplayed() } @@ -1005,7 +1005,7 @@ open class MessageList : override fun onBackStackChanged() { findFragments() - messageListFragment?.isActive = true + messageListFragment?.setFullyActive() if (isDrawerEnabled && !isAdditionalMessageListDisplayed) { unlockDrawer() @@ -1032,7 +1032,7 @@ open class MessageList : } messageListFragment = fragment - fragment.isActive = true + fragment.setFullyActive() if (isDrawerEnabled) { lockDrawer() @@ -1178,12 +1178,13 @@ open class MessageList : messageViewOnly = false messageListWasDisplayed = true displayMode = DisplayMode.MESSAGE_LIST - viewSwitcher!!.showFirstView() messageViewContainerFragment?.isActive = false messageListFragment!!.isActive = true messageListFragment!!.setActiveMessage(null) + viewSwitcher!!.showFirstView() + setDrawerLockState() showDefaultTitleView() @@ -1233,6 +1234,7 @@ open class MessageList : override fun onSwitchComplete(displayedChild: Int) { if (displayedChild == 0) { removeMessageViewContainerFragment() + messageListFragment?.onFullyActive() } } @@ -1323,6 +1325,11 @@ open class MessageList : } } + private fun MessageListFragment.setFullyActive() { + isActive = true + onFullyActive() + } + private fun configureDrawer() { val drawer = drawer ?: return drawer.selectAccount(account!!.uuid) @@ -1347,11 +1354,6 @@ open class MessageList : permissionUiHelper.requestPermission(permission) } - override fun onFolderNotFoundError() { - val defaultFolderId = defaultFolderProvider.getDefaultFolder(account!!) - openFolderImmediately(defaultFolderId) - } - private enum class DisplayMode { MESSAGE_LIST, MESSAGE_VIEW, SPLIT_VIEW } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageListActivityConfig.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageListActivityConfig.kt index 1f910f91e..ba099bca5 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageListActivityConfig.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageListActivityConfig.kt @@ -17,6 +17,7 @@ data class MessageListActivityConfig( val isShowContactPicture: Boolean, val isColorizeMissingContactPictures: Boolean, val isUseBackgroundAsUnreadIndicator: Boolean, + val isShowComposeButton: Boolean, val contactNameColor: Int, val messageViewTheme: SubTheme, val messageListPreviewLines: Int, @@ -48,6 +49,7 @@ data class MessageListActivityConfig( isShowContactPicture = K9.isShowContactPicture, isColorizeMissingContactPictures = K9.isColorizeMissingContactPictures, isUseBackgroundAsUnreadIndicator = K9.isUseBackgroundAsUnreadIndicator, + isShowComposeButton = K9.isShowComposeButtonOnMessageList, contactNameColor = K9.contactNameColor, messageViewTheme = settings.messageViewTheme, messageListPreviewLines = K9.messageListPreviewLines, diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/ThemeExtensions.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/ThemeExtensions.kt index d8aa489e0..51c73e2d0 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/ThemeExtensions.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/ThemeExtensions.kt @@ -1,6 +1,7 @@ package com.fsck.k9.ui import android.content.res.Resources.Theme +import android.graphics.Color import android.graphics.drawable.Drawable import android.util.TypedValue @@ -15,6 +16,32 @@ fun Theme.resolveColorAttribute(attrId: Int): Int { return typedValue.data } +fun Theme.resolveColorAttribute(colorAttrId: Int, alphaFractionAttrId: Int, backgroundColorAttrId: Int): Int { + val typedValue = TypedValue() + + if (!resolveAttribute(colorAttrId, typedValue, true)) { + error("Couldn't resolve attribute ($colorAttrId)") + } + val color = typedValue.data + + if (!resolveAttribute(alphaFractionAttrId, typedValue, true)) { + error("Couldn't resolve attribute ($alphaFractionAttrId)") + } + val colorPercentage = TypedValue.complexToFloat(typedValue.data) + val backgroundPercentage = 1 - colorPercentage + + if (!resolveAttribute(backgroundColorAttrId, typedValue, true)) { + error("Couldn't resolve attribute ($colorAttrId)") + } + val backgroundColor = typedValue.data + + val red = colorPercentage * Color.red(color) + backgroundPercentage * Color.red(backgroundColor) + val green = colorPercentage * Color.green(color) + backgroundPercentage * Color.green(backgroundColor) + val blue = colorPercentage * Color.blue(color) + backgroundPercentage * Color.blue(backgroundColor) + + return Color.rgb(red.toInt(), green.toInt(), blue.toInt()) +} + fun Theme.resolveDrawableAttribute(attrId: Int): Drawable { val typedValue = TypedValue() diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/RecentChangesViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/RecentChangesViewModel.kt index e3d8c6c54..c106e8738 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/RecentChangesViewModel.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/RecentChangesViewModel.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.map @OptIn(ExperimentalCoroutinesApi::class) class RecentChangesViewModel( private val generalSettingsManager: GeneralSettingsManager, - changeLogManager: ChangeLogManager + private val changeLogManager: ChangeLogManager ) : ViewModel() { val shouldShowRecentChangesHint = changeLogManager.changeLogFlow.flatMapLatest { changeLog -> if (changeLog.isFirstRun && !changeLog.isFirstRunEver) { @@ -28,4 +28,8 @@ class RecentChangesViewModel( .map { generalSettings -> generalSettings.showRecentChanges } .distinctUntilChanged() } + + fun onRecentChangesHintDismissed() { + changeLogManager.writeCurrentVersion() + } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/choosefolder/ChooseFolderActivity.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/choosefolder/ChooseFolderActivity.kt index e6e77e3f0..5ec07b4ac 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/choosefolder/ChooseFolderActivity.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/choosefolder/ChooseFolderActivity.kt @@ -212,6 +212,7 @@ class ChooseFolderActivity : K9Activity() { val locale = Locale.getDefault() val displayName = item.displayName.lowercase(locale) return constraint.splitToSequence(" ") + .filter { it.isNotEmpty() } .map { it.lowercase(locale) } .any { it in displayName } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/fab/ShrinkFabOnScrollListener.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/fab/ShrinkFabOnScrollListener.kt new file mode 100644 index 000000000..f6eee590e --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/fab/ShrinkFabOnScrollListener.kt @@ -0,0 +1,39 @@ +package com.fsck.k9.ui.fab + +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.OnScrollListener +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton + +/** + * Shrink the supplied [ExtendedFloatingActionButton] when the RecyclerView this listener is attached to is scrolling + * down, and expand the FAB when scrolling up. + */ +class ShrinkFabOnScrollListener(private val floatingActionButton: ExtendedFloatingActionButton) : OnScrollListener() { + private var isScrolledUp = true + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + if (dy > 0) { + if (recyclerView.canScrollVertically(1)) { + shrink() + } else { + extend() + } + } else if (dy < 0) { + extend() + } + } + + private fun extend() { + if (!isScrolledUp) { + isScrolledUp = true + floatingActionButton.extend() + } + } + + private fun shrink() { + if (isScrolledUp) { + isScrolledUp = false + floatingActionButton.shrink() + } + } +} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt index b58281a03..18c937c28 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt @@ -51,8 +51,16 @@ class MessageListAdapter internal constructor( private val answeredIcon: Drawable = theme.resolveDrawableAttribute(R.attr.messageListAnswered) private val forwardedAnsweredIcon: Drawable = theme.resolveDrawableAttribute(R.attr.messageListAnsweredForwarded) private val previewTextColor: Int = theme.resolveColorAttribute(R.attr.messageListPreviewTextColor) - private val activeItemBackgroundColor: Int = theme.resolveColorAttribute(R.attr.messageListActiveItemBackgroundColor) - private val selectedItemBackgroundColor: Int = theme.resolveColorAttribute(R.attr.messageListSelectedBackgroundColor) + private val activeItemBackgroundColor: Int = theme.resolveColorAttribute( + colorAttrId = R.attr.messageListActiveItemBackgroundColor, + alphaFractionAttrId = R.attr.messageListActiveItemBackgroundAlphaFraction, + backgroundColorAttrId = R.attr.messageListActiveItemBackgroundAlphaBackground + ) + private val selectedItemBackgroundColor: Int = theme.resolveColorAttribute( + colorAttrId = R.attr.messageListSelectedBackgroundColor, + alphaFractionAttrId = R.attr.messageListSelectedBackgroundAlphaFraction, + backgroundColorAttrId = R.attr.messageListSelectedBackgroundAlphaBackground + ) private val regularItemBackgroundColor: Int = theme.resolveColorAttribute(R.attr.messageListRegularItemBackgroundColor) private val readItemBackgroundColor: Int = theme.resolveColorAttribute(R.attr.messageListReadItemBackgroundColor) private val unreadItemBackgroundColor: Int = theme.resolveColorAttribute(R.attr.messageListUnreadItemBackgroundColor) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt index 660c52f94..51390db73 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt @@ -11,9 +11,14 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.widget.TextView import android.widget.Toast +import androidx.annotation.StringRes import androidx.appcompat.view.ActionMode import androidx.core.os.bundleOf +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.core.view.setPadding import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.recyclerview.widget.RecyclerView @@ -46,10 +51,13 @@ import com.fsck.k9.ui.R import com.fsck.k9.ui.changelog.RecentChangesActivity import com.fsck.k9.ui.changelog.RecentChangesViewModel import com.fsck.k9.ui.choosefolder.ChooseFolderActivity +import com.fsck.k9.ui.fab.ShrinkFabOnScrollListener import com.fsck.k9.ui.folders.FolderNameFormatter import com.fsck.k9.ui.folders.FolderNameFormatterFactory import com.fsck.k9.ui.helper.RelativeDateTimeFormatter import com.fsck.k9.ui.messagelist.MessageListFragment.MessageListFragmentListener.Companion.MAX_PROGRESS +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback import com.google.android.material.snackbar.Snackbar import java.util.concurrent.Future import net.jcip.annotations.GuardedBy @@ -85,6 +93,7 @@ class MessageListFragment : private var recyclerView: RecyclerView? = null private var itemTouchHelper: ItemTouchHelper? = null private var swipeRefreshLayout: SwipeRefreshLayout? = null + private var floatingActionButton: ExtendedFloatingActionButton? = null private lateinit var adapter: MessageListAdapter @@ -100,6 +109,7 @@ class MessageListFragment : private var sortDateAscending = false private var actionMode: ActionMode? = null private var hasConnectivity: Boolean? = null + private var isShowFloatingActionButton: Boolean = true /** * Relevant messages for the current context when we have to remember the chosen messages @@ -132,6 +142,8 @@ class MessageListFragment : */ private var isInitialized = false + private var error: Error? = null + /** * Set this to `true` when the fragment should be considered active. When active, the fragment adds its actions to * the toolbar. When inactive, the fragment won't add its actions to the toolbar, even it is still visible, e.g. as @@ -142,6 +154,7 @@ class MessageListFragment : field = value resetActionMode() invalidateMenu() + maybeHideFloatingActionButton() } val isShowAccountChip: Boolean @@ -162,7 +175,11 @@ class MessageListFragment : setHasOptionsMenu(true) restoreInstanceState(savedInstanceState) - decodeArguments() ?: return + val error = decodeArguments() + if (error != null) { + this.error = error + return + } viewModel.getMessageListLiveData().observe(this) { messageListInfo: MessageListInfo -> setMessageList(messageListInfo) @@ -187,7 +204,7 @@ class MessageListFragment : rememberedSelected = savedInstanceState.getLongArray(STATE_SELECTED_MESSAGES)?.toSet() } - private fun decodeArguments(): MessageListFragment? { + private fun decodeArguments(): Error? { val arguments = requireArguments() showingThreadedList = arguments.getBoolean(ARG_THREADED_LIST, false) isThreadDisplay = arguments.getBoolean(ARG_IS_THREAD_DISPLAY, false) @@ -213,12 +230,11 @@ class MessageListFragment : currentFolder = getFolderInfoHolder(folderId, account!!) isSingleFolderMode = true } catch (e: MessagingException) { - fragmentListener.onFolderNotFoundError() - return null + return Error.FolderNotFound } } - return this + return null } private fun createMessageListAdapter(): MessageListAdapter { @@ -236,11 +252,29 @@ class MessageListFragment : } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.message_list_fragment, container, false) + return if (error == null) { + inflater.inflate(R.layout.message_list_fragment, container, false) + } else { + inflater.inflate(R.layout.message_list_error, container, false) + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (error == null) { + initializeMessageListLayout(view) + } else { + initializeErrorLayout(view) + } + } + + private fun initializeErrorLayout(view: View) { + val errorMessageView = view.findViewById(R.id.message_list_error_message) + errorMessageView.text = getString(error!!.errorText) + } + + private fun initializeMessageListLayout(view: View) { initializeSwipeRefreshLayout(view) + initializeFloatingActionButton(view) initializeRecyclerView(view) initializeRecentChangesSnackbar() @@ -265,9 +299,40 @@ class MessageListFragment : this.swipeRefreshLayout = swipeRefreshLayout } + private fun initializeFloatingActionButton(view: View) { + isShowFloatingActionButton = K9.isShowComposeButtonOnMessageList + if (isShowFloatingActionButton) { + enableFloatingActionButton(view) + } else { + disableFloatingActionButton(view) + } + } + + private fun enableFloatingActionButton(view: View) { + val floatingActionButton = view.findViewById(R.id.floating_action_button) + + floatingActionButton.setOnClickListener { + onCompose() + } + + val recyclerView = view.findViewById(R.id.message_list) + recyclerView.addOnScrollListener(ShrinkFabOnScrollListener(floatingActionButton)) + + this.floatingActionButton = floatingActionButton + } + + private fun disableFloatingActionButton(view: View) { + val floatingActionButton = view.findViewById(R.id.floating_action_button) + floatingActionButton.isGone = true + } + private fun initializeRecyclerView(view: View) { val recyclerView = view.findViewById(R.id.message_list) + if (!isShowFloatingActionButton) { + recyclerView.setPadding(0) + } + val itemDecoration = MessageListItemDecoration(requireContext()) recyclerView.addItemDecoration(itemDecoration) @@ -308,6 +373,13 @@ class MessageListFragment : recentChangesSnackbar = Snackbar .make(coordinatorLayout, R.string.changelog_snackbar_text, Snackbar.LENGTH_INDEFINITE) .setAction(R.string.okay_action) { launchRecentChangesActivity() } + .addCallback(object : BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + if (event == DISMISS_EVENT_SWIPE) { + recentChangesViewModel.onRecentChangesHintDismissed() + } + } + }) recentChangesViewModel.shouldShowRecentChangesHint .observe(viewLifecycleOwner, shouldShowRecentChangesHintObserver) @@ -356,7 +428,12 @@ class MessageListFragment : } fun updateTitle() { - if (!isInitialized) return + if (error != null) { + fragmentListener.setMessageListTitle(getString(R.string.message_list_error_title)) + return + } else if (!isInitialized) { + return + } setWindowTitle() @@ -463,6 +540,7 @@ class MessageListFragment : recyclerView = null itemTouchHelper = null swipeRefreshLayout = null + floatingActionButton = null if (isNewMessagesView && !requireActivity().isChangingConfigurations) { messagingController.clearNewMessages(account) @@ -474,6 +552,8 @@ class MessageListFragment : override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) + if (error != null) return + outState.putLongArray(STATE_SELECTED_MESSAGES, adapter.selected.toLongArray()) outState.putBoolean(STATE_REMOTE_SEARCH_PERFORMED, isRemoteSearch) outState.putStringArray( @@ -726,7 +806,7 @@ class MessageListFragment : } override fun onPrepareOptionsMenu(menu: Menu) { - if (isActive) { + if (isActive && error == null) { prepareMenu(menu) } else { hideMenu(menu) @@ -734,10 +814,9 @@ class MessageListFragment : } private fun prepareMenu(menu: Menu) { - menu.findItem(R.id.compose).isVisible = true + menu.findItem(R.id.compose).isVisible = !isShowFloatingActionButton menu.findItem(R.id.set_sort).isVisible = true menu.findItem(R.id.select_all).isVisible = true - menu.findItem(R.id.compose).isVisible = true menu.findItem(R.id.mark_all_as_read).isVisible = isMarkAllAsReadSupported menu.findItem(R.id.empty_trash).isVisible = isShowingTrashFolder @@ -1435,6 +1514,18 @@ class MessageListFragment : } } + fun onFullyActive() { + maybeShowFloatingActionButton() + } + + private fun maybeShowFloatingActionButton() { + floatingActionButton?.isVisible = true + } + + private fun maybeHideFloatingActionButton() { + floatingActionButton?.isGone = true + } + // For the last N displayed messages we remember the original 'read' and 'starred' state of the messages. We pass // this information to MessageListLoader so messages can be sorted according to these remembered values and not the // current state. This way messages, that are marked as read/unread or starred/not starred while being displayed, @@ -1935,17 +2026,20 @@ class MessageListFragment : COPY, MOVE } + private enum class Error(@StringRes val errorText: Int) { + FolderNotFound(R.string.message_list_error_folder_not_found) + } + interface MessageListFragmentListener { fun setMessageListProgressEnabled(enable: Boolean) fun setMessageListProgress(level: Int) fun showThread(account: Account, threadRootId: Long) fun openMessage(messageReference: MessageReference) - fun setMessageListTitle(title: String, subtitle: String?) + fun setMessageListTitle(title: String, subtitle: String? = null) fun onCompose(account: Account?) fun startSearch(query: String, account: Account?, folderId: Long?): Boolean fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? fun goBack() - fun onFolderNotFoundError() companion object { const val MAX_PROGRESS = 10000 diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/onboarding/WelcomeFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/onboarding/WelcomeFragment.kt index 55080bc02..046f48a51 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/onboarding/WelcomeFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/onboarding/WelcomeFragment.kt @@ -15,11 +15,11 @@ import com.fsck.k9.ui.observeNotNull import com.fsck.k9.ui.settings.import.SettingsImportResultViewModel import com.fsck.k9.ui.settings.import.SettingsImportSuccess import org.koin.android.ext.android.inject -import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import org.koin.androidx.viewmodel.ext.android.activityViewModel class WelcomeFragment : Fragment() { private val htmlToSpanned: HtmlToSpanned by inject() - private val importResultViewModel: SettingsImportResultViewModel by sharedViewModel() + private val importResultViewModel: SettingsImportResultViewModel by activityViewModel() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_welcome_message, container, false) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsFragment.kt index b79ef7170..8ae3d7cab 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsFragment.kt @@ -39,14 +39,14 @@ import com.fsck.k9.ui.settings.removeEntry import com.fsck.k9.ui.withArguments import com.takisoft.preferencex.PreferenceFragmentCompat import org.koin.android.ext.android.inject -import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import org.koin.androidx.viewmodel.ext.android.activityViewModel import org.koin.core.parameter.parametersOf import org.openintents.openpgp.OpenPgpApiManager import org.openintents.openpgp.util.OpenPgpKeyPreference import org.openintents.openpgp.util.OpenPgpProviderUtil class AccountSettingsFragment : PreferenceFragmentCompat(), ConfirmationDialogFragmentListener { - private val viewModel: AccountSettingsViewModel by sharedViewModel() + private val viewModel: AccountSettingsViewModel by activityViewModel() private val dataStoreFactory: AccountSettingsDataStoreFactory by inject() private val openPgpApiManager: OpenPgpApiManager by inject { parametersOf(this) } private val messagingController: MessagingController by inject() diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt index f74519418..8308befea 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt @@ -31,6 +31,7 @@ class GeneralSettingsDataStore( "messagelist_show_contact_picture" -> K9.isShowContactPicture "messagelist_colorize_missing_contact_pictures" -> K9.isColorizeMissingContactPictures "messagelist_background_as_unread_indicator" -> K9.isUseBackgroundAsUnreadIndicator + "show_compose_button" -> K9.isShowComposeButtonOnMessageList "threaded_view" -> K9.isThreadedViewEnabled "messageview_fixedwidth_font" -> K9.isUseMessageViewFixedWidthFont "messageview_autofit_width" -> K9.isAutoFitWidth @@ -61,6 +62,7 @@ class GeneralSettingsDataStore( "messagelist_show_contact_picture" -> K9.isShowContactPicture = value "messagelist_colorize_missing_contact_pictures" -> K9.isColorizeMissingContactPictures = value "messagelist_background_as_unread_indicator" -> K9.isUseBackgroundAsUnreadIndicator = value + "show_compose_button" -> K9.isShowComposeButtonOnMessageList = value "threaded_view" -> K9.isThreadedViewEnabled = value "messageview_fixedwidth_font" -> K9.isUseMessageViewFixedWidthFont = value "messageview_autofit_width" -> K9.isAutoFitWidth = value diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/import/SettingsImportFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/import/SettingsImportFragment.kt index 330b1b571..4b3399dcb 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/import/SettingsImportFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/import/SettingsImportFragment.kt @@ -19,12 +19,12 @@ import com.fsck.k9.ui.R import com.fsck.k9.ui.observeNotNull import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.adapters.ItemAdapter -import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import org.koin.androidx.viewmodel.ext.android.activityViewModel import org.koin.androidx.viewmodel.ext.android.viewModel class SettingsImportFragment : Fragment() { private val viewModel: SettingsImportViewModel by viewModel() - private val resultViewModel: SettingsImportResultViewModel by sharedViewModel() + private val resultViewModel: SettingsImportResultViewModel by activityViewModel() private lateinit var settingsImportAdapter: FastAdapter> private lateinit var itemAdapter: ItemAdapter> diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/view/ViewSwitcher.java b/app/ui/legacy/src/main/java/com/fsck/k9/view/ViewSwitcher.java index 137a70de4..bbeb40d88 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/view/ViewSwitcher.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/view/ViewSwitcher.java @@ -30,6 +30,7 @@ public class ViewSwitcher extends ViewAnimator implements AnimationListener { public void showFirstView() { if (getDisplayedChild() == 0) { + onAnimationEnd(null); return; } @@ -40,6 +41,7 @@ public class ViewSwitcher extends ViewAnimator implements AnimationListener { public void showSecondView() { if (getDisplayedChild() == 1) { + onAnimationEnd(null); return; } diff --git a/app/ui/legacy/src/main/res/drawable/ic_check_circle_large.xml b/app/ui/legacy/src/main/res/drawable/ic_check_circle_large.xml index 6483898ca..52c9bcc0d 100644 --- a/app/ui/legacy/src/main/res/drawable/ic_check_circle_large.xml +++ b/app/ui/legacy/src/main/res/drawable/ic_check_circle_large.xml @@ -6,7 +6,7 @@ + + + + + + + diff --git a/app/ui/legacy/src/main/res/layout/message_list_fragment.xml b/app/ui/legacy/src/main/res/layout/message_list_fragment.xml index b37ab8db6..975566b9a 100644 --- a/app/ui/legacy/src/main/res/layout/message_list_fragment.xml +++ b/app/ui/legacy/src/main/res/layout/message_list_fragment.xml @@ -1,5 +1,6 @@ + + diff --git a/app/ui/legacy/src/main/res/raw/changelog_master.xml b/app/ui/legacy/src/main/res/raw/changelog_master.xml index c989d4196..3c93a611e 100644 --- a/app/ui/legacy/src/main/res/raw/changelog_master.xml +++ b/app/ui/legacy/src/main/res/raw/changelog_master.xml @@ -5,6 +5,26 @@ Locale-specific versions are kept in res/raw-/changelog.xml. --> + + Fixed crash at app startup + + + Delete spam messages immediately without moving them to the trash folder + Changed the way home screen widgets are disabled when there is no account set up to work around a bug in Android versions prior to 12 + Mark recent changes as read when the snackbar is dismissed via swipe + + + The light and dark themes are now based on Material Design 2. This is still work in progress. + Added a floating compose button to the message list screen + Search now also considers recipient addresses + Always move to previous/next message when swiping left/right outside of the message body + Lists of folders are now sorted alphabetically in account settings + Added better support for right-to-left languages when composing messages + When sending a message, the associated draft message will be deleted immediately, bypassing the Trash folder + mailto:, matrix:, and xmpp: URIs in plain text messages are now turned into links + Fixed a bug where notifications would sometimes reappear shortly after having been dismissed + Various other bug fixes + Added swipe actions to the message list screen Added support for swiping between messages diff --git a/app/ui/legacy/src/main/res/values/attrs.xml b/app/ui/legacy/src/main/res/values/attrs.xml index 6b9ee5322..b98c894c9 100644 --- a/app/ui/legacy/src/main/res/values/attrs.xml +++ b/app/ui/legacy/src/main/res/values/attrs.xml @@ -6,6 +6,8 @@ + + @@ -67,13 +69,18 @@ + + + + + diff --git a/app/ui/legacy/src/main/res/values/dimensions.xml b/app/ui/legacy/src/main/res/values/dimensions.xml index e47b20f37..8f5f3d1dd 100644 --- a/app/ui/legacy/src/main/res/values/dimensions.xml +++ b/app/ui/legacy/src/main/res/values/dimensions.xml @@ -12,4 +12,8 @@ 72dp 24dp 12dp + + 16dp + + 80dp diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml index f66041fb6..a00a8e8dd 100644 --- a/app/ui/legacy/src/main/res/values/strings.xml +++ b/app/ui/legacy/src/main/res/values/strings.xml @@ -1299,4 +1299,12 @@ You can keep this message and use it as a backup for your secret key. If you wan Spam Move… + + + Show floating compose button + + + Error + + Folder not found diff --git a/app/ui/legacy/src/main/res/values/themes.xml b/app/ui/legacy/src/main/res/values/themes.xml index 8c89ec5b8..5e4087128 100644 --- a/app/ui/legacy/src/main/res/values/themes.xml +++ b/app/ui/legacy/src/main/res/values/themes.xml @@ -16,13 +16,16 @@ @color/material_gray_100 @color/material_gray_100 - @color/material_blue_600 - @color/material_blue_800 - @color/material_pink_400 - @color/material_pink_200 + @color/material_gray_800 + @color/material_gray_700 + @color/material_pink_500 + @color/material_pink_300 + #ffffff @color/material_gray_200 @color/material_gray_100 @color/material_gray_50 + ?attr/colorPrimary + ?attr/colorOnPrimary @style/Widget.K9.Toolbar @style/PreferenceThemeOverlay @@ -88,13 +91,18 @@ @drawable/ic_import_status @android:color/primary_text_light @android:color/secondary_text_light - #ff99d9ee + ?attr/colorSecondary + ?attr/colorSecondaryVariant + 33% + ?attr/colorSurface ?android:attr/windowBackground #ffd8d8d8 ?attr/messageListRegularItemBackgroundColor ?android:attr/colorBackground @drawable/thread_count_box_light - #ff2ea7d1 + ?attr/colorSecondaryVariant + 60% + ?attr/colorSurface #ff696969 #ffcccccc #bbbbbb @@ -171,13 +179,15 @@ @color/material_gray_900 @color/material_gray_900 - @color/material_blue_400 - @color/material_blue_600 + @color/material_gray_100 + @color/material_gray_50 @color/material_pink_300 @color/material_pink_500 @color/material_gray_900 @color/material_gray_900 @color/material_gray_900 + ?attr/colorPrimary + ?attr/colorOnPrimary @style/Widget.K9.Toolbar @style/PreferenceThemeOverlay @@ -243,13 +253,18 @@ @drawable/ic_import_status @android:color/primary_text_dark @android:color/secondary_text_dark - #ff347489 + ?attr/colorSecondary + ?attr/colorSecondaryVariant + 25% + ?attr/colorSurface ?android:attr/windowBackground ?attr/messageListRegularItemBackgroundColor #ff505050 ?android:attr/colorBackground @drawable/thread_count_box_dark - #ff33b5e5 + ?attr/colorSecondaryVariant + 50% + ?attr/colorSurface #ffa0a0a0 #ff333333 #777777 diff --git a/app/ui/legacy/src/main/res/xml/general_settings.xml b/app/ui/legacy/src/main/res/xml/general_settings.xml index 845181625..a197ce237 100644 --- a/app/ui/legacy/src/main/res/xml/general_settings.xml +++ b/app/ui/legacy/src/main/res/xml/general_settings.xml @@ -248,6 +248,10 @@ android:summary="@string/global_settings_threaded_view_summary" android:title="@string/global_settings_threaded_view_label" /> + + diff --git a/app/ui/setup/build.gradle b/app/ui/setup/build.gradle index 092bc160b..3585042da 100644 --- a/app/ui/setup/build.gradle +++ b/app/ui/setup/build.gradle @@ -1,5 +1,7 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} dependencies { api project(":app:ui:base") @@ -7,23 +9,23 @@ dependencies { implementation project(":app:autodiscovery:api") implementation project(":mail:common") - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidxLifecycle}" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}" - implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}" - implementation "androidx.core:core-ktx:${versions.androidxCore}" - implementation "com.jakewharton.timber:timber:${versions.timber}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.kotlinCoroutines}" + implementation libs.androidx.lifecycle.viewmodel.ktx + implementation libs.androidx.lifecycle.livedata.ktx + implementation libs.androidx.constraintlayout + implementation libs.androidx.core.ktx + implementation libs.timber + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android testImplementation project(':mail:testing') testImplementation project(':app:testing') - 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:${versions.koin}" - testImplementation "io.insert-koin:koin-test-junit4:${versions.koin}" + testImplementation libs.robolectric + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.core + testImplementation libs.mockito.kotlin + testImplementation libs.koin.test + testImplementation libs.koin.test.junit4 } android { diff --git a/backend/api/build.gradle b/backend/api/build.gradle index 13eabe22f..aa86ea20a 100644 --- a/backend/api/build.gradle +++ b/backend/api/build.gradle @@ -1,6 +1,8 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} dependencies { api project(":mail:common") diff --git a/backend/demo/build.gradle b/backend/demo/build.gradle index 00031f89e..4e1ea924c 100644 --- a/backend/demo/build.gradle +++ b/backend/demo/build.gradle @@ -1,17 +1,19 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.google.devtools.ksp' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.ksp) + alias(libs.plugins.android.lint) +} dependencies { api project(":backend:api") - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}" - implementation "com.squareup.moshi:moshi:${versions.moshi}" - ksp "com.squareup.moshi:moshi-kotlin-codegen:${versions.moshi}" + implementation libs.kotlinx.coroutines.core + implementation libs.moshi + ksp libs.moshi.kotlin.codegen testImplementation project(":mail:testing") - testImplementation "junit:junit:${versions.junit}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" - testImplementation "com.google.truth:truth:${versions.truth}" + testImplementation libs.junit + testImplementation libs.mockito.core + testImplementation libs.truth } diff --git a/backend/imap/build.gradle b/backend/imap/build.gradle index 013afae0f..745177e04 100644 --- a/backend/imap/build.gradle +++ b/backend/imap/build.gradle @@ -1,19 +1,21 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} dependencies { api project(":backend:api") api project(":mail:protocols:imap") api project(":mail:protocols:smtp") - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}" + implementation libs.kotlinx.coroutines.core testImplementation project(":mail:testing") testImplementation project(":backend:testing") - testImplementation "junit:junit:${versions.junit}" - testImplementation "org.mockito:mockito-inline:${versions.mockito}" - testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" - testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.apache.james:apache-mime4j-dom:${versions.mime4j}" + testImplementation libs.junit + testImplementation libs.mockito.inline + testImplementation libs.mockito.kotlin + testImplementation libs.truth + testImplementation libs.mime4j.dom } diff --git a/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapSync.kt b/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapSync.kt index 1d62f180d..66d703092 100644 --- a/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapSync.kt +++ b/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapSync.kt @@ -627,7 +627,7 @@ internal class ImapSync( ) { /* * The provider was unable to get the structure of the message, so - * we'll download a reasonable portion of the messge and mark it as + * we'll download a reasonable portion of the message and mark it as * incomplete so the entire thing can be downloaded later if the user * wishes to download it. */ diff --git a/backend/jmap/build.gradle b/backend/jmap/build.gradle index 318691869..700abb790 100644 --- a/backend/jmap/build.gradle +++ b/backend/jmap/build.gradle @@ -1,18 +1,20 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.google.devtools.ksp' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.ksp) + alias(libs.plugins.android.lint) +} dependencies { api project(":backend:api") - api "com.squareup.okhttp3:okhttp:${versions.okhttp}" - implementation "rs.ltt.jmap:jmap-client:0.3.1" - implementation "com.squareup.moshi:moshi:${versions.moshi}" - ksp "com.squareup.moshi:moshi-kotlin-codegen:${versions.moshi}" + api libs.okhttp + implementation libs.jmap.client + implementation libs.moshi + ksp libs.moshi.kotlin.codegen testImplementation project(":mail:testing") testImplementation project(':backend:testing') - testImplementation "org.mockito:mockito-core:${versions.mockito}" - testImplementation("com.squareup.okhttp3:mockwebserver:${versions.okhttp}") + testImplementation libs.mockito.core + testImplementation libs.okhttp.mockwebserver } diff --git a/backend/pop3/build.gradle b/backend/pop3/build.gradle index 40731fbf7..dfd503ead 100644 --- a/backend/pop3/build.gradle +++ b/backend/pop3/build.gradle @@ -1,6 +1,8 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} dependencies { api project(":backend:api") @@ -8,6 +10,6 @@ dependencies { api project(":mail:protocols:smtp") testImplementation project(":mail:testing") - testImplementation "junit:junit:${versions.junit}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" + testImplementation libs.junit + testImplementation libs.mockito.core } diff --git a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java index 3ba740fb3..8eae32f22 100644 --- a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java +++ b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java @@ -550,7 +550,7 @@ class Pop3Sync { Pop3Message message) throws MessagingException { /* * The provider was unable to get the structure of the message, so - * we'll download a reasonable portion of the messge and mark it as + * we'll download a reasonable portion of the message and mark it as * incomplete so the entire thing can be downloaded later if the user * wishes to download it. */ diff --git a/backend/testing/build.gradle b/backend/testing/build.gradle index f80583482..b1a4dddf5 100644 --- a/backend/testing/build.gradle +++ b/backend/testing/build.gradle @@ -1,10 +1,12 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} dependencies { implementation project(":backend:api") - implementation "com.squareup.okio:okio:${versions.okio}" - implementation "junit:junit:${versions.junit}" + implementation libs.okio + implementation libs.junit } diff --git a/backend/webdav/build.gradle b/backend/webdav/build.gradle index 18c1f091c..706ab2b7f 100644 --- a/backend/webdav/build.gradle +++ b/backend/webdav/build.gradle @@ -1,12 +1,14 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} dependencies { api project(":backend:api") api project(":mail:protocols:webdav") testImplementation project(":mail:testing") - testImplementation "junit:junit:${versions.junit}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" + testImplementation libs.junit + testImplementation libs.mockito.core } diff --git a/backend/webdav/src/main/AndroidManifest.xml b/backend/webdav/src/main/AndroidManifest.xml deleted file mode 100644 index 7299b9e31..000000000 --- a/backend/webdav/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavSync.java b/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavSync.java index e3bfff203..b11e5ca96 100644 --- a/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavSync.java +++ b/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavSync.java @@ -529,7 +529,7 @@ class WebDavSync { WebDavMessage message) throws MessagingException { /* * The provider was unable to get the structure of the message, so - * we'll download a reasonable portion of the messge and mark it as + * we'll download a reasonable portion of the message and mark it as * incomplete so the entire thing can be downloaded later if the user * wishes to download it. */ diff --git a/build.gradle b/build.gradle index befe8547d..963ccaa32 100644 --- a/build.gradle +++ b/build.gradle @@ -1,76 +1,15 @@ import com.android.build.gradle.BasePlugin -import org.gradle.api.plugins.JavaPlugin import org.jetbrains.kotlin.gradle.dsl.KotlinCompile -buildscript { - ext { - // Judging the impact of newer library versions on the app requires being intimately familiar with the code - // base. Please don't open pull requests upgrading dependencies if you're a new contributor. - versions = [ - 'kotlin': '1.7.22', - 'kotlinCoroutines': '1.6.4', - 'jetbrainsAnnotations': '23.0.0', - 'androidxAppCompat': '1.5.1', - 'androidxActivity': '1.6.0', - 'androidxRecyclerView': '1.2.1', - 'androidxLifecycle': '2.5.1', - 'androidxAnnotation': '1.5.0', - 'androidxBiometric': '1.1.0', - 'androidxNavigation': '2.5.2', - 'androidxConstraintLayout': '2.1.4', - 'androidxWorkManager': '2.7.1', - 'androidxFragment': '1.5.3', - 'androidxLocalBroadcastManager': '1.1.0', - 'androidxCore': '1.9.0', - 'androidxCardView': '1.0.0', - 'androidxPreference': '1.2.0', - 'androidxDrawerLayout': '1.1.1', - 'androidxTransition': '1.4.1', - 'androidxTestCore': '1.4.0', - 'materialComponents': '1.6.1', - 'fastAdapter': '5.7.0', - 'preferencesFix': '1.1.0', - 'okio': '3.2.0', - 'moshi': '1.14.0', - 'timber': '5.0.1', - 'koin': '3.2.2', - // We can't upgrade Commons IO beyond this version because starting with 2.7 it is using Java 8 API - // that is not available until Android API 26 (even with desugaring enabled). - // See https://issuetracker.google.com/issues/160484830 - 'commonsIo': '2.6', - 'mime4j': '0.8.6', - 'okhttp': '4.10.0', - 'minidns': '1.0.4', - 'glide': '4.14.2', - 'jsoup': '1.15.3', - 'httpClient': '4.5.13', - - 'androidxTestRunner': '1.4.0', - 'junit': '4.13.2', - 'robolectric': '4.9', - 'mockito': '4.8.0', - 'mockitoKotlin': '4.0.0', - 'truth': '1.1.3', - 'turbine': '0.11.0', - - 'ktlint': '0.44.0' - ] - - javaVersion = JavaVersion.VERSION_1_8 - } - - repositories { - mavenCentral() - google() - gradlePluginPortal() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" - classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:1.7.22-1.0.8" - classpath "org.jlleitschuh.gradle:ktlint-gradle:11.0.0" - } +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.android.lint) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.parcelize) apply false + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.ktlint) apply false } project.ext { @@ -80,23 +19,23 @@ project.ext { allprojects { configurations.all { resolutionStrategy.dependencySubstitution { - substitute module("androidx.core:core") using module("androidx.core:core:${versions.androidxCore}") - substitute module("androidx.activity:activity") using module("androidx.activity:activity:${versions.androidxActivity}") - substitute module("androidx.activity:activity-ktx") using module("androidx.activity:activity-ktx:${versions.androidxActivity}") - substitute module("androidx.fragment:fragment") using module("androidx.fragment:fragment:${versions.androidxFragment}") - substitute module("androidx.fragment:fragment-ktx") using module("androidx.fragment:fragment-ktx:${versions.androidxFragment}") - substitute module("androidx.appcompat:appcompat") using module("androidx.appcompat:appcompat:${versions.androidxAppCompat}") - substitute module("androidx.preference:preference") using module("androidx.preference:preference:${versions.androidxPreference}") - substitute module("androidx.recyclerview:recyclerview") using module("androidx.recyclerview:recyclerview:${versions.androidxRecyclerView}") - substitute module("androidx.constraintlayout:constraintlayout") using module("androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}") - substitute module("androidx.drawerlayout:drawerlayout") using module("androidx.drawerlayout:drawerlayout:${versions.androidxDrawerLayout}") - substitute module("androidx.lifecycle:lifecycle-livedata") using module("androidx.lifecycle:lifecycle-livedata:${versions.androidxLifecycle}") - substitute module("androidx.transition:transition") using module("androidx.transition:transition:${versions.androidxTransition}") - substitute module("org.jetbrains:annotations") using module("org.jetbrains:annotations:${versions.jetbrainsAnnotations}") - substitute module("org.jetbrains.kotlin:kotlin-stdlib") using module("org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}") - substitute module("org.jetbrains.kotlin:kotlin-stdlib-jdk7") using module("org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}") - substitute module("org.jetbrains.kotlin:kotlin-stdlib-jdk8") using module("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}") - substitute module("org.jetbrains.kotlinx:kotlinx-coroutines-android") using module("org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.kotlinCoroutines}") + substitute module("androidx.core:core") using module("androidx.core:core:${libs.versions.androidxCore.get()}") + substitute module("androidx.activity:activity") using module("androidx.activity:activity:${libs.versions.androidxActivity.get()}") + substitute module("androidx.activity:activity-ktx") using module("androidx.activity:activity-ktx:${libs.versions.androidxActivity.get()}") + substitute module("androidx.fragment:fragment") using module("androidx.fragment:fragment:${libs.versions.androidxFragment.get()}") + substitute module("androidx.fragment:fragment-ktx") using module("androidx.fragment:fragment-ktx:${libs.versions.androidxFragment.get()}") + substitute module("androidx.appcompat:appcompat") using module("androidx.appcompat:appcompat:${libs.versions.androidxAppCompat.get()}") + substitute module("androidx.preference:preference") using module("androidx.preference:preference:${libs.versions.androidxPreference.get()}") + substitute module("androidx.recyclerview:recyclerview") using module("androidx.recyclerview:recyclerview:${libs.versions.androidxRecyclerView.get()}") + substitute module("androidx.constraintlayout:constraintlayout") using module("androidx.constraintlayout:constraintlayout:${libs.versions.androidxConstraintLayout.get()}") + substitute module("androidx.drawerlayout:drawerlayout") using module("androidx.drawerlayout:drawerlayout:${libs.versions.androidxDrawerLayout.get()}") + substitute module("androidx.lifecycle:lifecycle-livedata") using module("androidx.lifecycle:lifecycle-livedata:${libs.versions.androidxLifecycle.get()}") + substitute module("androidx.transition:transition") using module("androidx.transition:transition:${libs.versions.androidxTransition.get()}") + substitute module("org.jetbrains:annotations") using module("org.jetbrains:annotations:${libs.versions.jetbrainsAnnotations.get()}") + substitute module("org.jetbrains.kotlin:kotlin-stdlib") using module("org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}") + substitute module("org.jetbrains.kotlin:kotlin-stdlib-jdk7") using module("org.jetbrains.kotlin:kotlin-stdlib-jdk7:${libs.versions.kotlin.get()}") + substitute module("org.jetbrains.kotlin:kotlin-stdlib-jdk8") using module("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${libs.versions.kotlin.get()}") + substitute module("org.jetbrains.kotlinx:kotlinx-coroutines-android") using module("org.jetbrains.kotlinx:kotlinx-coroutines-android:${libs.versions.kotlinCoroutines.get()}") } } @@ -113,8 +52,8 @@ allprojects { } compileOptions { - sourceCompatibility javaVersion - targetCompatibility javaVersion + sourceCompatibility libs.versions.java.get() + targetCompatibility libs.versions.java.get() } lintOptions { @@ -132,8 +71,8 @@ allprojects { plugins.withType(JavaPlugin).configureEach { project.java { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion + sourceCompatibility = libs.versions.java.get() + targetCompatibility = libs.versions.java.get() } } @@ -148,13 +87,13 @@ allprojects { tasks.withType(KotlinCompile) { kotlinOptions { - jvmTarget = javaVersion + jvmTarget = libs.versions.java.get() } } apply plugin: 'org.jlleitschuh.gradle.ktlint' ktlint { - version = versions.ktlint + version = libs.versions.ktlint.get() } } diff --git a/cli/html-cleaner-cli/build.gradle b/cli/html-cleaner-cli/build.gradle index 5d06b5f7e..26f2c9d59 100644 --- a/cli/html-cleaner-cli/build.gradle +++ b/cli/html-cleaner-cli/build.gradle @@ -1,5 +1,7 @@ -apply plugin: 'org.jetbrains.kotlin.jvm' -apply plugin: 'application' +plugins { + alias(libs.plugins.kotlin.jvm) + id 'application' +} version 'unspecified' @@ -10,6 +12,6 @@ application { dependencies { implementation project(':app:html-cleaner') - implementation "com.github.ajalt.clikt:clikt:3.4.0" - implementation "com.squareup.okio:okio:${versions.okio}" + implementation libs.clikt + implementation libs.okio } diff --git a/fastlane/metadata/android/en-US/changelogs/35000.txt b/fastlane/metadata/android/en-US/changelogs/35000.txt new file mode 100644 index 000000000..94845ff7b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/35000.txt @@ -0,0 +1,10 @@ +- The light and dark themes are now based on Material Design 2. This is still work in progress. +- Added a floating compose button to the message list screen +- Search now also considers recipient addresses +- Always move to previous/next message when swiping left/right outside of the message body +- Lists of folders are now sorted alphabetically in account settings +- Added better support for right-to-left languages when composing messages +- When sending a message, the associated draft message will be deleted immediately, bypassing the Trash folder +- mailto:, matrix:, and xmpp: URIs in plain text messages are now turned into links +- Fixed a bug where notifications would sometimes reappear shortly after having been dismissed +- Various other bug fixes diff --git a/fastlane/metadata/android/en-US/changelogs/35001.txt b/fastlane/metadata/android/en-US/changelogs/35001.txt new file mode 100644 index 000000000..3c25d9a7c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/35001.txt @@ -0,0 +1,3 @@ +- Delete spam messages immediately without moving them to the trash folder +- Changed the way home screen widgets are disabled when there is no account set up to work around a bug in Android versions prior to 12 +- Mark recent changes as read when the snackbar is dismissed via swipe diff --git a/fastlane/metadata/android/en-US/changelogs/35002.txt b/fastlane/metadata/android/en-US/changelogs/35002.txt new file mode 100644 index 000000000..dfd863970 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/35002.txt @@ -0,0 +1 @@ +- Fixed crash at app startup diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..e58b30439 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,125 @@ +# Judging the impact of newer library versions on the app requires being intimately familiar with the code base. +# Please don't open pull requests upgrading dependencies if you're a new contributor. + +[versions] +java = "11" +androidGradlePlugin = "7.4.0" +ktlint = "0.44.0" + +kotlin = "1.8.0" +kotlinCoroutines = "1.6.4" +jetbrainsAnnotations = "24.0.0" +androidxAppCompat = "1.6.0" +androidxActivity = "1.6.1" +androidxRecyclerView = "1.2.1" +androidxLifecycle = "2.5.1" +androidxNavigation = "2.5.3" +androidxConstraintLayout = "2.1.4" +androidxFragment = "1.5.5" +androidxCore = "1.9.0" +androidxPreference = "1.2.0" +androidxDrawerLayout = "1.1.1" +androidxTransition = "1.4.1" +fastAdapter = "5.7.0" +preferencesFix = "1.1.0" +timber = "5.0.1" +koinCore = "3.3.2" +koinAndroid = "3.3.2" +mime4j = "0.8.8" +okhttp = "4.10.0" +glide = "4.14.2" +moshi = "1.14.0" +mockito = "5.0.0" + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +android-lint = { id = "com.android.lint", version.ref = "androidGradlePlugin" } +ksp = "com.google.devtools.ksp:1.8.0-1.0.8" +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +ktlint = "org.jlleitschuh.gradle.ktlint:11.0.0" + +[libraries] +desugar = "com.android.tools:desugar_jdk_libs:1.1.8" + +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinCoroutines" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinCoroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinCoroutines" } +jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrainsAnnotations" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppCompat" } +androidx-activity = { module = "androidx.activity:activity", version.ref = "androidxActivity" } +androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "androidxRecyclerView" } +androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "androidxLifecycle" } +androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidxLifecycle" } +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } +androidx-annotation = "androidx.annotation:annotation:1.5.0" +androidx-biometric = "androidx.biometric:biometric:1.1.0" +androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "androidxNavigation" } +androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "androidxNavigation" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidxConstraintLayout" } +androidx-work-ktx = "androidx.work:work-runtime-ktx:2.7.1" +androidx-fragment = { module = "androidx.fragment:fragment", version.ref = "androidxFragment" } +androidx-localbroadcastmanager = "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0" +androidx-core = { module = "androidx.core:core", version.ref = "androidxCore" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" } +androidx-cardview = "androidx.cardview:cardview:1.0.0" +androidx-preference = { module = "androidx.preference:preference", version.ref = "androidxPreference" } +androidx-swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" +androidx-test-core = "androidx.test:core:1.5.0" +android-material = "com.google.android.material:material:1.7.0" +fastadapter = { module = "com.mikepenz:fastadapter", version.ref = "fastAdapter" } +fastadapter-extensions-drag = { module = "com.mikepenz:fastadapter-extensions-drag", version.ref = "fastAdapter" } +fastadapter-extensions-utils = { module = "com.mikepenz:fastadapter-extensions-utils", version.ref = "fastAdapter" } +materialdrawer = "com.mikepenz:materialdrawer:8.4.5" +preferencex = { module = "com.takisoft.preferencex:preferencex", version.ref = "preferencesFix" } +preferencex-datetimepicker = { module = "com.takisoft.preferencex:preferencex-datetimepicker", version.ref = "preferencesFix" } +preferencex-colorpicker = { module = "com.takisoft.preferencex:preferencex-colorpicker", version.ref = "preferencesFix" } +okio = "com.squareup.okio:okio:3.3.0" +moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } +moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } +timber = "com.jakewharton.timber:timber:5.0.1" +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinCore" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" } +koin-test = { module = "io.insert-koin:koin-test", version.ref = "koinCore" } +koin-test-junit4 = { module = "io.insert-koin:koin-test-junit4", version.ref = "koinCore" } +commons-io = "commons-io:commons-io:2.11.0" +mime4j-core = { module = "org.apache.james:apache-mime4j-core", version.ref = "mime4j" } +mime4j-dom = { module = "org.apache.james:apache-mime4j-dom", version.ref = "mime4j" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } +minidns-hla = "org.minidns:minidns-hla:1.0.4" +glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } +jsoup = "org.jsoup:jsoup:1.15.3" +apache-httpclient = "org.apache.httpcomponents:httpclient:4.5.13" +apache-httpclient5 = "org.apache.httpcomponents.client5:httpclient5:5.1.3" +clikt = "com.github.ajalt.clikt:clikt:3.5.1" +jzlib = "com.jcraft:jzlib:1.0.7" +jutf7 = "com.beetstra.jutf7:jutf7:1.0.0" +jcip-annotations = "net.jcip:jcip-annotations:1.0" +jmap-client = "rs.ltt.jmap:jmap-client:0.3.1" +circleimageview = "de.hdodenhof:circleimageview:3.1.0" +appauth = "net.openid:appauth:0.11.1" +searchPreference = "com.github.ByteHamster:SearchPreference:v2.3.0" +safeContentResolver = "de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0" +tokenautocomplete = "com.splitwise:tokenautocomplete:4.0.0-beta01" +ckchangelog-core = "de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02" +xmlpull = "com.github.cketti:xmlpull-extracted-from-android:1.0" +kxml2 = "com.github.cketti:kxml2-extracted-from-android:1.0" + +junit = "junit:junit:4.13.2" +robolectric = "org.robolectric:robolectric:4.9.2" +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } +mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito" } +mockito-kotlin = "org.mockito.kotlin:mockito-kotlin:4.1.0" +truth = "com.google.truth:truth:1.1.3" +turbine = "app.cash.turbine:turbine:0.12.1" +jdom2 = "org.jdom:jdom2:2.0.6.1" +icu4j-charset = "com.ibm.icu:icu4j-charset:72.1" + +leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.9.1" diff --git a/mail/common/build.gradle b/mail/common/build.gradle index 4681b3d0d..e6e6d57cb 100644 --- a/mail/common/build.gradle +++ b/mail/common/build.gradle @@ -1,27 +1,29 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} if (rootProject.testCoverage) { apply plugin: 'jacoco' } dependencies { - api "org.jetbrains:annotations:${versions.jetbrainsAnnotations}" + api libs.jetbrains.annotations - implementation "org.apache.james:apache-mime4j-core:${versions.mime4j}" - implementation "org.apache.james:apache-mime4j-dom:${versions.mime4j}" - implementation "com.squareup.okio:okio:${versions.okio}" - implementation "commons-io:commons-io:${versions.commonsIo}" - implementation "com.squareup.moshi:moshi:${versions.moshi}" + implementation libs.mime4j.core + implementation libs.mime4j.dom + implementation libs.okio + implementation libs.commons.io + implementation libs.moshi // We're only using this for its DefaultHostnameVerifier - implementation "org.apache.httpcomponents.client5:httpclient5:5.1.3" + implementation libs.apache.httpclient5 testImplementation project(":mail:testing") - testImplementation "junit:junit:${versions.junit}" - testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-inline:${versions.mockito}" - testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" - testImplementation "com.ibm.icu:icu4j-charset:70.1" + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.inline + testImplementation libs.mockito.kotlin + testImplementation libs.icu4j.charset } diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java index da138d2fa..da697b12a 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java @@ -149,9 +149,13 @@ public class MimeUtility { } else if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding)) { inputStream = new QuotedPrintableInputStream(rawInputStream) { @Override - public void close() throws IOException { + public void close() { super.close(); - closeInputStreamWithoutDeletingTemporaryFiles(rawInputStream); + try { + closeInputStreamWithoutDeletingTemporaryFiles(rawInputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } } }; } else { diff --git a/mail/protocols/imap/build.gradle b/mail/protocols/imap/build.gradle index ee26ab9a6..eea587364 100644 --- a/mail/protocols/imap/build.gradle +++ b/mail/protocols/imap/build.gradle @@ -1,6 +1,8 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} if (rootProject.testCoverage) { apply plugin: 'jacoco' @@ -9,16 +11,16 @@ if (rootProject.testCoverage) { dependencies { api project(":mail:common") - implementation "com.jcraft:jzlib:1.0.7" - implementation "com.beetstra.jutf7:jutf7:1.0.0" - implementation "commons-io:commons-io:${versions.commonsIo}" - implementation "com.squareup.okio:okio:${versions.okio}" + implementation libs.jzlib + implementation libs.jutf7 + implementation libs.commons.io + implementation libs.okio testImplementation project(":mail:testing") - 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 "com.squareup.okio:okio:${versions.okio}" - testImplementation "org.apache.james:apache-mime4j-core:${versions.mime4j}" + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.core + testImplementation libs.mockito.kotlin + testImplementation libs.okio + testImplementation libs.mime4j.core } diff --git a/mail/protocols/pop3/build.gradle b/mail/protocols/pop3/build.gradle index 67ef9f6cd..8ff2e742c 100644 --- a/mail/protocols/pop3/build.gradle +++ b/mail/protocols/pop3/build.gradle @@ -1,6 +1,8 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} if (rootProject.testCoverage) { apply plugin: 'jacoco' @@ -10,11 +12,11 @@ dependencies { api project(":mail:common") testImplementation project(":mail:testing") - 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 "com.squareup.okio:okio:${versions.okio}" - testImplementation "com.jcraft:jzlib:1.0.7" - testImplementation "commons-io:commons-io:${versions.commonsIo}" + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.core + testImplementation libs.mockito.kotlin + testImplementation libs.okio + testImplementation libs.jzlib + testImplementation libs.commons.io } diff --git a/mail/protocols/smtp/build.gradle b/mail/protocols/smtp/build.gradle index 8e8adb312..b4f19fab0 100644 --- a/mail/protocols/smtp/build.gradle +++ b/mail/protocols/smtp/build.gradle @@ -1,6 +1,8 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} if (rootProject.testCoverage) { apply plugin: 'jacoco' @@ -9,13 +11,13 @@ if (rootProject.testCoverage) { dependencies { api project(":mail:common") - implementation "commons-io:commons-io:${versions.commonsIo}" - implementation "com.squareup.okio:okio:${versions.okio}" + implementation libs.commons.io + implementation libs.okio testImplementation project(":mail:testing") - testImplementation "junit:junit:${versions.junit}" - testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" - testImplementation "com.squareup.okio:okio:${versions.okio}" - testImplementation "com.jcraft:jzlib:1.0.7" + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.kotlin + testImplementation libs.okio + testImplementation libs.jzlib } diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt index 0591d664e..773e515e3 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt @@ -283,8 +283,9 @@ class SmtpTransport( logResponse(smtpResponse) } - private fun logResponse(smtpResponse: SmtpResponse, omitText: Boolean = false) { + private fun logResponse(smtpResponse: SmtpResponse, sensitive: Boolean = false) { if (K9MailLib.isDebug()) { + val omitText = sensitive && !K9MailLib.isDebugSensitive() Timber.v("%s", smtpResponse.toLogString(omitText, linePrefix = "SMTP <<< ")) } } @@ -532,7 +533,7 @@ class SmtpTransport( repeat(pipelinedCommands.size) { val response = responseParser.readResponse(isEnhancedStatusCodesProvided) - logResponse(response, omitText = false) + logResponse(response) if (response.isNegativeResponse && firstException == null) { firstException = buildNegativeSmtpReplyException(response) diff --git a/mail/protocols/webdav/build.gradle b/mail/protocols/webdav/build.gradle index be69d3a64..24eaada2d 100644 --- a/mail/protocols/webdav/build.gradle +++ b/mail/protocols/webdav/build.gradle @@ -1,5 +1,7 @@ -apply plugin: 'java-library' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + alias(libs.plugins.android.lint) +} if (rootProject.testCoverage) { apply plugin: 'jacoco' @@ -8,12 +10,12 @@ if (rootProject.testCoverage) { dependencies { api project(":mail:common") - implementation "commons-io:commons-io:${versions.commonsIo}" - compileOnly "org.apache.httpcomponents:httpclient:${versions.httpClient}" + implementation libs.commons.io + compileOnly libs.apache.httpclient testImplementation project(":mail:testing") - testImplementation "junit:junit:${versions.junit}" - testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-inline:${versions.mockito}" - testImplementation "org.apache.httpcomponents:httpclient:${versions.httpClient}" + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.inline + testImplementation libs.apache.httpclient } diff --git a/mail/testing/build.gradle b/mail/testing/build.gradle index 2c4e34f8a..d09cf6891 100644 --- a/mail/testing/build.gradle +++ b/mail/testing/build.gradle @@ -1,6 +1,8 @@ -apply plugin: 'java-library' -apply plugin: 'kotlin' -apply plugin: 'com.android.lint' +plugins { + id 'java-library' + id 'kotlin' + alias(libs.plugins.android.lint) +} if (rootProject.testCoverage) { apply plugin: 'jacoco' @@ -9,6 +11,6 @@ if (rootProject.testCoverage) { dependencies { api project(":mail:common") - api "com.squareup.okio:okio:${versions.okio}" - api "junit:junit:${versions.junit}" + api libs.okio + api libs.junit } diff --git a/plugins/openpgp-api-lib/openpgp-api/build.gradle b/plugins/openpgp-api-lib/openpgp-api/build.gradle index 7ba7c0b8f..c0f8a0e18 100644 --- a/plugins/openpgp-api-lib/openpgp-api/build.gradle +++ b/plugins/openpgp-api-lib/openpgp-api/build.gradle @@ -1,4 +1,6 @@ -apply plugin: 'com.android.library' +plugins { + alias(libs.plugins.android.library) +} android { namespace 'org.openintents.openpgp' @@ -9,7 +11,7 @@ android { } dependencies { - implementation "androidx.lifecycle:lifecycle-common:${versions.androidxLifecycle}" - implementation "com.jakewharton.timber:timber:${versions.timber}" - implementation "com.takisoft.preferencex:preferencex:${versions.preferencesFix}" + implementation libs.androidx.lifecycle.common + implementation libs.timber + implementation libs.preferencex } diff --git a/settings.gradle b/settings.gradle index c51c5f87a..f8ae7c284 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,11 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { diff --git a/ui-utils/ItemTouchHelper/build.gradle b/ui-utils/ItemTouchHelper/build.gradle index 7c8d85a0d..b53ba4165 100644 --- a/ui-utils/ItemTouchHelper/build.gradle +++ b/ui-utils/ItemTouchHelper/build.gradle @@ -1,7 +1,9 @@ -apply plugin: 'com.android.library' +plugins { + alias(libs.plugins.android.library) +} dependencies { - api "androidx.recyclerview:recyclerview:${versions.androidxRecyclerView}" + api libs.androidx.recyclerview } android { diff --git a/ui-utils/LinearLayoutManager/build.gradle b/ui-utils/LinearLayoutManager/build.gradle index 96cb59147..53ab0bd9a 100644 --- a/ui-utils/LinearLayoutManager/build.gradle +++ b/ui-utils/LinearLayoutManager/build.gradle @@ -1,7 +1,9 @@ -apply plugin: 'com.android.library' +plugins { + alias(libs.plugins.android.library) +} dependencies { - api "androidx.recyclerview:recyclerview:${versions.androidxRecyclerView}" + api libs.androidx.recyclerview } android { diff --git a/ui-utils/ToolbarBottomSheet/build.gradle b/ui-utils/ToolbarBottomSheet/build.gradle index c6ff351b8..370922cd7 100644 --- a/ui-utils/ToolbarBottomSheet/build.gradle +++ b/ui-utils/ToolbarBottomSheet/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' dependencies { - api "com.google.android.material:material:${versions.materialComponents}" + api libs.android.material } android {