Merge branch 'main' into message-view-redesign

This commit is contained in:
cketti 2023-01-18 12:56:25 +01:00
commit 7dd0ed79c4
99 changed files with 1496 additions and 1040 deletions

View file

@ -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")

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -1,26 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- We initialize WorkManager manually -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
</application>
</manifest>

View file

@ -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)

View file

@ -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<LocalMessage> messages) {
private void deleteThreadsSynchronous(Account account, long folderId, List<LocalMessage> messages, boolean skipTrashFolder) {
try {
List<LocalMessage> 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<MessageReference> messages) {
deleteMessages(messages, false);
}
private void deleteMessages(List<MessageReference> 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<LocalMessage> messages) {
private void deleteMessagesSynchronous(Account account, long folderId, List<LocalMessage> messages, boolean skipTrashFolder) {
try {
List<LocalMessage> localOnlyMessages = new ArrayList<>();
List<LocalMessage> syncedMessages = new ArrayList<>();
@ -2012,8 +2024,10 @@ public class MessagingController {
Map<String, String> 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.");

View file

@ -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).
*
* <p>
* Result is also trimmed.

View file

@ -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) }
}
}

View file

@ -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<WorkerFactory> { K9WorkerFactory(get(), get()) }
single { get<WorkManagerProvider>().getWorkManager() }
single { K9JobManager(get(), get(), get()) }
single { WorkManagerConfigurationProvider(workerFactory = get()) }
single<WorkerFactory> { K9WorkerFactory() }
single { WorkManager.getInstance(get<Context>()) }
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)
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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);
}
}
};
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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)?")
}
}

View file

@ -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,
)
}

View file

@ -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);

View file

@ -36,7 +36,7 @@ public class Settings {
*
* @see SettingsExporter
*/
public static final int VERSION = 84;
public static final int VERSION = 85;
static Map<String, Object> validate(int version, Map<String, TreeMap<Integer, SettingsDescription>> settings,
Map<String, String> importedSettings, boolean useDefaultValues) {

View file

@ -241,7 +241,7 @@ public class LocalSearch implements SearchSpecification {
}
///////////////////////////////////////////////////////////////
// Public accesor methods
// Public accessor methods
///////////////////////////////////////////////////////////////
/**
* TODO THIS HAS TO GO!!!!

View file

@ -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<NotificationActionCreator>() }
single { mock<NotificationStrategy>() }
single(named("controllerExtensions")) { emptyList<ControllerExtension>() }
single { mock<WorkManager>() }
}

View file

@ -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);
}
}

View file

@ -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)
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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)
}
}
}

View file

@ -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 {

View file

@ -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);

View file

@ -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
}

View file

@ -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 {

View file

@ -175,6 +175,9 @@
</intent-filter>
</activity>
<!--
This component is disabled by default. It will be enabled programmatically after an account has been set up.
-->
<activity
android:name=".activity.MessageCompose"
android:configChanges="locale"
@ -224,10 +227,14 @@
android:resource="@xml/searchable"/>
</activity>
<!--
This component is disabled by default. It will be enabled programmatically after an account has been set up.
-->
<activity
android:name=".activity.LauncherShortcuts"
android:configChanges="locale"
android:label="@string/shortcuts_title"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT"/>
@ -287,10 +294,12 @@
android:name=".activity.setup.OAuthFlowActivity"
android:label="@string/account_setup_basics_title" />
<!-- This component is disabled by default (if possible). It will be enabled programmatically if necessary. -->
<receiver
android:name=".provider.UnreadWidgetProvider"
android:icon="@drawable/ic_launcher"
android:label="@string/unread_widget_label"
android:enabled="@bool/home_screen_widgets_enabled"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@ -300,10 +309,12 @@
android:resource="@xml/unread_widget_info"/>
</receiver>
<!-- This component is disabled by default (if possible). It will be enabled programmatically if necessary. -->
<receiver
android:name=".widget.list.MessageListWidgetProvider"
android:icon="@drawable/message_list_widget_preview"
android:label="@string/mail_list_widget_text"
android:enabled="@bool/home_screen_widgets_enabled"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -313,6 +324,7 @@
android:resource="@xml/message_list_widget_info" />
</receiver>
<!-- This component is disabled by default. It will be enabled programmatically if necessary. -->
<receiver
android:name=".controller.push.BootCompleteReceiver"
android:exported="false"
@ -323,8 +335,7 @@
</receiver>
<service
android:name=".notification.NotificationActionService"
android:enabled="true"/>
android:name=".notification.NotificationActionService"/>
<service
android:name=".service.DatabaseUpgradeService"
@ -399,5 +410,19 @@
</intent-filter>
</activity>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- We're using on-demand initialization for WorkManager -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
</application>
</manifest>

View file

@ -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,
)
)
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Home screen widgets should be disabled by default. They will be enabled programmatically if necessary. -->
<bool name="home_screen_widgets_enabled">false</bool>
</resources>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
We'd like to disable this component by default. However, due to a bug in Android versions prior to 12, users then
wouldn't be able to use the home screen widget.
See https://android.googlesource.com/platform/frameworks/base/+/85be035336af8d83eb24980026418207c85991cb%5E%21/#F0
-->
<bool name="home_screen_widgets_enabled">true</bool>
</resources>

View file

@ -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<FolderNameFormatter> { RuntimeEnvironment.getApplication() }
withParameter<SizeFormatter> { RuntimeEnvironment.getApplication() }
withParameter<ChangelogViewModel> { ChangeLogMode.CHANGE_LOG }
withParameter<MailSyncWorker> { mock<WorkerParameters>() }
}
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -3,6 +3,9 @@
<application>
<!--
This component is disabled by default. It will be enabled programmatically by SystemLocaleManager if necessary.
-->
<receiver
android:name=".locale.LocaleBroadcastReceiver"
android:exported="false"

View file

@ -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(":app:ui:base")
@ -17,57 +19,57 @@ dependencies {
implementation project(':plugins:openpgp-api-lib:openpgp-api')
implementation "androidx.appcompat:appcompat:${versions.androidxAppCompat}"
implementation "androidx.preference:preference:${versions.androidxPreference}"
implementation "com.takisoft.preferencex:preferencex:${versions.preferencesFix}"
implementation "com.takisoft.preferencex:preferencex-datetimepicker:${versions.preferencesFix}"
implementation "com.takisoft.preferencex:preferencex-colorpicker:${versions.preferencesFix}"
implementation "androidx.recyclerview:recyclerview:${versions.androidxRecyclerView}"
implementation libs.androidx.appcompat
implementation libs.androidx.preference
implementation libs.preferencex
implementation libs.preferencex.datetimepicker
implementation libs.preferencex.colorpicker
implementation libs.androidx.recyclerview
implementation project(':ui-utils:LinearLayoutManager')
implementation project(':ui-utils:ItemTouchHelper')
implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidxLifecycle}"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidxLifecycle}"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}"
implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}"
implementation "androidx.cardview:cardview:${versions.androidxCardView}"
implementation "androidx.localbroadcastmanager:localbroadcastmanager:${versions.androidxLocalBroadcastManager}"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02"
implementation "com.splitwise:tokenautocomplete:4.0.0-beta01"
implementation "de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0"
implementation 'com.mikepenz:materialdrawer:8.4.5'
implementation 'com.github.ByteHamster:SearchPreference:v2.3.0'
implementation "com.mikepenz:fastadapter:${versions.fastAdapter}"
implementation "com.mikepenz:fastadapter-extensions-drag:${versions.fastAdapter}"
implementation "com.mikepenz:fastadapter-extensions-utils:${versions.fastAdapter}"
implementation 'de.hdodenhof:circleimageview:3.1.0'
api 'net.openid:appauth:0.11.1'
implementation libs.androidx.lifecycle.runtime.ktx
implementation libs.androidx.lifecycle.viewmodel.ktx
implementation libs.androidx.lifecycle.livedata.ktx
implementation libs.androidx.constraintlayout
implementation libs.androidx.cardview
implementation libs.androidx.localbroadcastmanager
implementation libs.androidx.swiperefreshlayout
implementation libs.ckchangelog.core
implementation libs.tokenautocomplete
implementation libs.safeContentResolver
implementation libs.materialdrawer
implementation libs.searchPreference
implementation libs.fastadapter
implementation libs.fastadapter.extensions.drag
implementation libs.fastadapter.extensions.utils
implementation libs.circleimageview
api libs.appauth
implementation "commons-io:commons-io:${versions.commonsIo}"
implementation "androidx.core:core-ktx:${versions.androidxCore}"
implementation "net.jcip:jcip-annotations:1.0"
implementation "com.jakewharton.timber:timber:${versions.timber}"
implementation "org.apache.james:apache-mime4j-core:${versions.mime4j}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.kotlinCoroutines}"
implementation libs.commons.io
implementation libs.androidx.core.ktx
implementation libs.jcip.annotations
implementation libs.timber
implementation libs.mime4j.core
implementation libs.kotlinx.coroutines.core
implementation libs.kotlinx.coroutines.android
implementation "com.github.bumptech.glide:glide:${versions.glide}"
annotationProcessor "com.github.bumptech.glide:compiler:${versions.glide}"
implementation libs.glide
annotationProcessor libs.glide.compiler
testImplementation project(':mail:testing')
testImplementation project(':app:storage')
testImplementation project(':app:testing')
testImplementation "org.robolectric:robolectric:${versions.robolectric}"
testImplementation "androidx.test:core:${versions.androidxTestCore}"
testImplementation "junit:junit:${versions.junit}"
testImplementation "org.jetbrains.kotlin:kotlin-test:${versions.kotlin}"
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 "org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.kotlinCoroutines}"
testImplementation "app.cash.turbine:turbine:${versions.turbine}"
testImplementation libs.robolectric
testImplementation libs.androidx.test.core
testImplementation libs.junit
testImplementation libs.kotlin.test
testImplementation libs.truth
testImplementation libs.mockito.inline
testImplementation libs.mockito.kotlin
testImplementation libs.koin.test
testImplementation libs.koin.test.junit4
testImplementation libs.kotlinx.coroutines.test
testImplementation libs.turbine
}
android {

View file

@ -1500,7 +1500,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
messagingController.sendMessage(account, message, plaintextSubject, null);
if (draftId != null) {
// TODO set draft id to invalid in MessageCompose!
messagingController.deleteDraft(account, draftId);
messagingController.deleteDraftSkippingTrashFolder(account, draftId);
}
return null;

View file

@ -341,7 +341,7 @@ open class MessageList :
val messageListFragment = checkNotNull(this.messageListFragment)
messageListWasDisplayed = true
messageListFragment.isActive = true
messageListFragment.setFullyActive()
messageViewContainerFragment.let { messageViewContainerFragment ->
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
}

View file

@ -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,

View file

@ -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()

View file

@ -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()
}
}

View file

@ -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 }
}

View file

@ -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()
}
}
}

View file

@ -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)

View file

@ -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<TextView>(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<ExtendedFloatingActionButton>(R.id.floating_action_button)
floatingActionButton.setOnClickListener {
onCompose()
}
val recyclerView = view.findViewById<RecyclerView>(R.id.message_list)
recyclerView.addOnScrollListener(ShrinkFabOnScrollListener(floatingActionButton))
this.floatingActionButton = floatingActionButton
}
private fun disableFloatingActionButton(view: View) {
val floatingActionButton = view.findViewById<ExtendedFloatingActionButton>(R.id.floating_action_button)
floatingActionButton.isGone = true
}
private fun initializeRecyclerView(view: View) {
val recyclerView = view.findViewById<RecyclerView>(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<Snackbar>() {
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

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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<ImportListItem<*>>
private lateinit var itemAdapter: ItemAdapter<ImportListItem<*>>

View file

@ -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;
}

View file

@ -6,7 +6,7 @@
<path
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
android:strokeWidth="1"
android:fillColor="#1976d2"
android:fillColor="?attr/messageListSelectedCheckMarkColor"
android:strokeColor="#00000000"/>
<path
android:pathData="m16.795,23.875 l-4.17,-4.17 -1.42,1.41 5.59,5.59 12,-12 -1.41,-1.41z"

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/message_list_error"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/message_list_error_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:contentDescription="@null"
android:src="@drawable/ic_error"
app:layout_constraintBottom_toTopOf="@+id/message_list_error_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/message_list_error_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:textAppearance="?attr/textAppearanceBody1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/message_list_error_icon"
tools:text="@string/message_list_error_folder_not_found" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/message_list_coordinator"
android:layout_width="match_parent"
@ -15,11 +16,26 @@
android:id="@+id/message_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fadingEdge="none"
android:scrollbarStyle="insideOverlay"
android:paddingBottom="@dimen/floatingActionButtonSpacing"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
tools:listitem="@layout/message_list_item" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/floating_action_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/floatingActionButtonMargin"
android:contentDescription="@string/compose_action"
android:text="@string/compose_action"
android:textColor="?attr/floatingActionButtonForegroundColor"
app:backgroundTint="?attr/floatingActionButtonBackgroundColor"
app:icon="?attr/iconActionCompose"
app:iconTint="?attr/floatingActionButtonForegroundColor" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -5,6 +5,26 @@
Locale-specific versions are kept in res/raw-<locale qualifier>/changelog.xml.
-->
<changelog>
<release version="6.502" versioncode="35002" date="2023-01-16">
<change>Fixed crash at app startup</change>
</release>
<release version="6.501" versioncode="35001" date="2023-01-09">
<change>Delete spam messages immediately without moving them to the trash folder</change>
<change>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</change>
<change>Mark recent changes as read when the snackbar is dismissed via swipe</change>
</release>
<release version="6.500" versioncode="35000" date="2023-01-06">
<change>The light and dark themes are now based on Material Design 2. This is still work in progress.</change>
<change>Added a floating compose button to the message list screen</change>
<change>Search now also considers recipient addresses</change>
<change>Always move to previous/next message when swiping left/right outside of the message body</change>
<change>Lists of folders are now sorted alphabetically in account settings</change>
<change>Added better support for right-to-left languages when composing messages</change>
<change>When sending a message, the associated draft message will be deleted immediately, bypassing the Trash folder</change>
<change>mailto:, matrix:, and xmpp: URIs in plain text messages are now turned into links</change>
<change>Fixed a bug where notifications would sometimes reappear shortly after having been dismissed</change>
<change>Various other bug fixes</change>
</release>
<release version="6.400" versioncode="34000" date="2022-11-28">
<change>Added swipe actions to the message list screen</change>
<change>Added support for swiping between messages</change>

View file

@ -6,6 +6,8 @@
<attr name="messageHeaderBackground" format="reference|color" />
<attr name="extraMessageHeaderBackground" format="reference|color" />
<attr name="bottomBarBackground" format="reference|color" />
<attr name="floatingActionButtonBackgroundColor" format="reference|color" />
<attr name="floatingActionButtonForegroundColor" format="reference|color" />
<attr name="iconUnifiedInbox" format="reference" />
<attr name="iconFolder" format="reference" />
<attr name="iconFolderInbox" format="reference" />
@ -67,13 +69,18 @@
<attr name="textColorPrimaryRecipientDropdown" format="reference" />
<attr name="textColorSecondaryRecipientDropdown" format="reference" />
<attr name="backgroundColorChooseAccountHeader" format="color" />
<attr name="messageListSelectedCheckMarkColor" format="reference|color"/>
<attr name="messageListSelectedBackgroundColor" format="reference|color"/>
<attr name="messageListSelectedBackgroundAlphaFraction" format="fraction"/>
<attr name="messageListSelectedBackgroundAlphaBackground" format="reference|color"/>
<attr name="messageListRegularItemBackgroundColor" format="reference|color"/>
<attr name="messageListReadItemBackgroundColor" format="reference|color"/>
<attr name="messageListUnreadItemBackgroundColor" format="reference|color"/>
<attr name="messageListThreadCountForegroundColor" format="reference|color"/>
<attr name="messageListThreadCountBackground" format="reference|color"/>
<attr name="messageListActiveItemBackgroundColor" format="reference|color"/>
<attr name="messageListActiveItemBackgroundAlphaFraction" format="fraction"/>
<attr name="messageListActiveItemBackgroundAlphaBackground" format="reference|color"/>
<attr name="messageListPreviewTextColor" format="reference|color"/>
<attr name="messageListDividerColor" format="reference|color"/>
<attr name="messageListStateIconTint" format="reference|color"/>

View file

@ -12,4 +12,8 @@
<dimen name="messageListSwipeThreshold">72dp</dimen>
<dimen name="messageListSwipeIconPadding">24dp</dimen>
<dimen name="messageListSwipeTextPadding">12dp</dimen>
<dimen name="floatingActionButtonMargin">16dp</dimen>
<!-- Height of ExtendedFloatingActionButton (48dp) plus two times floatingActionButtonMargin -->
<dimen name="floatingActionButtonSpacing">80dp</dimen>
</resources>

View file

@ -1299,4 +1299,12 @@ You can keep this message and use it as a backup for your secret key. If you wan
<string name="swipe_action_spam">Spam</string>
<!-- Name of the swipe action to move a message. The ellipsis (…) indicates that there is another step (selecting a folder) before the action is performed. Try to keep it short. -->
<string name="swipe_action_move">Move…</string>
<!-- Name of setting to configure whether to show a "compose" floating action button on top of the message list -->
<string name="general_settings_show_compose_button_title">Show floating compose button</string>
<!-- Displayed in the toolbar when there was an error loading the message list, e.g. because the folder no longer exists. -->
<string name="message_list_error_title">Error</string>
<!-- Displayed instead of the message list when a folder couldn't be found, e.g. due to an outdated home screen shortcut. -->
<string name="message_list_error_folder_not_found">Folder not found</string>
</resources>

View file

@ -16,13 +16,16 @@
<item name="android:statusBarColor">@color/material_gray_100</item>
<item name="toolbarColor">@color/material_gray_100</item>
<item name="colorPrimary">@color/material_blue_600</item>
<item name="colorPrimaryVariant">@color/material_blue_800</item>
<item name="colorSecondary">@color/material_pink_400</item>
<item name="colorSecondaryVariant">@color/material_pink_200</item>
<item name="colorPrimary">@color/material_gray_800</item>
<item name="colorPrimaryVariant">@color/material_gray_700</item>
<item name="colorSecondary">@color/material_pink_500</item>
<item name="colorSecondaryVariant">@color/material_pink_300</item>
<item name="colorOnSecondary">#ffffff</item>
<item name="messageHeaderBackground">@color/material_gray_200</item>
<item name="extraMessageHeaderBackground">@color/material_gray_100</item>
<item name="bottomBarBackground">@color/material_gray_50</item>
<item name="floatingActionButtonBackgroundColor">?attr/colorPrimary</item>
<item name="floatingActionButtonForegroundColor">?attr/colorOnPrimary</item>
<item name="toolbarStyle">@style/Widget.K9.Toolbar</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
@ -88,13 +91,18 @@
<item name="iconSettingsImportStatus">@drawable/ic_import_status</item>
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_light</item>
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_light</item>
<item name="messageListSelectedBackgroundColor">#ff99d9ee</item>
<item name="messageListSelectedCheckMarkColor">?attr/colorSecondary</item>
<item name="messageListSelectedBackgroundColor">?attr/colorSecondaryVariant</item>
<item name="messageListSelectedBackgroundAlphaFraction">33%</item>
<item name="messageListSelectedBackgroundAlphaBackground">?attr/colorSurface</item>
<item name="messageListRegularItemBackgroundColor">?android:attr/windowBackground</item>
<item name="messageListReadItemBackgroundColor">#ffd8d8d8</item>
<item name="messageListUnreadItemBackgroundColor">?attr/messageListRegularItemBackgroundColor</item>
<item name="messageListThreadCountForegroundColor">?android:attr/colorBackground</item>
<item name="messageListThreadCountBackground">@drawable/thread_count_box_light</item>
<item name="messageListActiveItemBackgroundColor">#ff2ea7d1</item>
<item name="messageListActiveItemBackgroundColor">?attr/colorSecondaryVariant</item>
<item name="messageListActiveItemBackgroundAlphaFraction">60%</item>
<item name="messageListActiveItemBackgroundAlphaBackground">?attr/colorSurface</item>
<item name="messageListPreviewTextColor">#ff696969</item>
<item name="messageListDividerColor">#ffcccccc</item>
<item name="messageListStateIconTint">#bbbbbb</item>
@ -171,13 +179,15 @@
<item name="android:statusBarColor">@color/material_gray_900</item>
<item name="toolbarColor">@color/material_gray_900</item>
<item name="colorPrimary">@color/material_blue_400</item>
<item name="colorPrimaryVariant">@color/material_blue_600</item>
<item name="colorPrimary">@color/material_gray_100</item>
<item name="colorPrimaryVariant">@color/material_gray_50</item>
<item name="colorSecondary">@color/material_pink_300</item>
<item name="colorSecondaryVariant">@color/material_pink_500</item>
<item name="messageHeaderBackground">@color/material_gray_900</item>
<item name="extraMessageHeaderBackground">@color/material_gray_900</item>
<item name="bottomBarBackground">@color/material_gray_900</item>
<item name="floatingActionButtonBackgroundColor">?attr/colorPrimary</item>
<item name="floatingActionButtonForegroundColor">?attr/colorOnPrimary</item>
<item name="toolbarStyle">@style/Widget.K9.Toolbar</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
@ -243,13 +253,18 @@
<item name="iconSettingsImportStatus">@drawable/ic_import_status</item>
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_dark</item>
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_dark</item>
<item name="messageListSelectedBackgroundColor">#ff347489</item>
<item name="messageListSelectedCheckMarkColor">?attr/colorSecondary</item>
<item name="messageListSelectedBackgroundColor">?attr/colorSecondaryVariant</item>
<item name="messageListSelectedBackgroundAlphaFraction">25%</item>
<item name="messageListSelectedBackgroundAlphaBackground">?attr/colorSurface</item>
<item name="messageListRegularItemBackgroundColor">?android:attr/windowBackground</item>
<item name="messageListReadItemBackgroundColor">?attr/messageListRegularItemBackgroundColor</item>
<item name="messageListUnreadItemBackgroundColor">#ff505050</item>
<item name="messageListThreadCountForegroundColor">?android:attr/colorBackground</item>
<item name="messageListThreadCountBackground">@drawable/thread_count_box_dark</item>
<item name="messageListActiveItemBackgroundColor">#ff33b5e5</item>
<item name="messageListActiveItemBackgroundColor">?attr/colorSecondaryVariant</item>
<item name="messageListActiveItemBackgroundAlphaFraction">50%</item>
<item name="messageListActiveItemBackgroundAlphaBackground">?attr/colorSurface</item>
<item name="messageListPreviewTextColor">#ffa0a0a0</item>
<item name="messageListDividerColor">#ff333333</item>
<item name="messageListStateIconTint">#777777</item>

View file

@ -248,6 +248,10 @@
android:summary="@string/global_settings_threaded_view_summary"
android:title="@string/global_settings_threaded_view_label" />
<CheckBoxPreference
android:key="show_compose_button"
android:title="@string/general_settings_show_compose_button_title" />
<ListPreference
android:dialogTitle="@string/global_settings_splitview_mode_label"
android:entries="@array/splitview_mode_entries"

View file

@ -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:ui:legacy")
implementation project(":app:core")
implementation "com.jakewharton.timber:timber:${versions.timber}"
implementation libs.timber
}
android {

View file

@ -4,7 +4,6 @@
<service
android:name=".MessageListWidgetService"
android:enabled="true"
android:permission="android.permission.BIND_REMOTEVIEWS" />
</application>

View file

@ -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 {

View file

@ -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")

View file

@ -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
}

View file

@ -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
}

View file

@ -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.
*/

View file

@ -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
}

View file

@ -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
}

View file

@ -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.
*/

View file

@ -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
}

View file

@ -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
}

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.fsck.k9.backend.webdav" />

View file

@ -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.
*/

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
- Fixed crash at app startup

125
gradle/libs.versions.toml Normal file
View file

@ -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"

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -1,3 +1,11 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {