Merge branch 'main' into message-view-redesign
This commit is contained in:
commit
7dd0ed79c4
99 changed files with 1496 additions and 1040 deletions
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.");
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)?")
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -241,7 +241,7 @@ public class LocalSearch implements SearchSpecification {
|
|||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// Public accesor methods
|
||||
// Public accessor methods
|
||||
///////////////////////////////////////////////////////////////
|
||||
/**
|
||||
* TODO THIS HAS TO GO!!!!
|
||||
|
|
|
@ -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>() }
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
5
app/k9mail/src/main/res/values-v31/manifest_values.xml
Normal file
5
app/k9mail/src/main/res/values-v31/manifest_values.xml
Normal 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>
|
9
app/k9mail/src/main/res/values/manifest_values.xml
Normal file
9
app/k9mail/src/main/res/values/manifest_values.xml
Normal 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>
|
|
@ -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>() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<*>>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
39
app/ui/legacy/src/main/res/layout/message_list_error.xml
Normal file
39
app/ui/legacy/src/main/res/layout/message_list_error.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
<service
|
||||
android:name=".MessageListWidgetService"
|
||||
android:enabled="true"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
|
||||
</application>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.fsck.k9.backend.webdav" />
|
|
@ -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.
|
||||
*/
|
||||
|
|
125
build.gradle
125
build.gradle
|
@ -1,76 +1,15 @@
|
|||
import com.android.build.gradle.BasePlugin
|
||||
import org.gradle.api.plugins.JavaPlugin
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinCompile
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
// Judging the impact of newer library versions on the app requires being intimately familiar with the code
|
||||
// base. Please don't open pull requests upgrading dependencies if you're a new contributor.
|
||||
versions = [
|
||||
'kotlin': '1.7.22',
|
||||
'kotlinCoroutines': '1.6.4',
|
||||
'jetbrainsAnnotations': '23.0.0',
|
||||
'androidxAppCompat': '1.5.1',
|
||||
'androidxActivity': '1.6.0',
|
||||
'androidxRecyclerView': '1.2.1',
|
||||
'androidxLifecycle': '2.5.1',
|
||||
'androidxAnnotation': '1.5.0',
|
||||
'androidxBiometric': '1.1.0',
|
||||
'androidxNavigation': '2.5.2',
|
||||
'androidxConstraintLayout': '2.1.4',
|
||||
'androidxWorkManager': '2.7.1',
|
||||
'androidxFragment': '1.5.3',
|
||||
'androidxLocalBroadcastManager': '1.1.0',
|
||||
'androidxCore': '1.9.0',
|
||||
'androidxCardView': '1.0.0',
|
||||
'androidxPreference': '1.2.0',
|
||||
'androidxDrawerLayout': '1.1.1',
|
||||
'androidxTransition': '1.4.1',
|
||||
'androidxTestCore': '1.4.0',
|
||||
'materialComponents': '1.6.1',
|
||||
'fastAdapter': '5.7.0',
|
||||
'preferencesFix': '1.1.0',
|
||||
'okio': '3.2.0',
|
||||
'moshi': '1.14.0',
|
||||
'timber': '5.0.1',
|
||||
'koin': '3.2.2',
|
||||
// We can't upgrade Commons IO beyond this version because starting with 2.7 it is using Java 8 API
|
||||
// that is not available until Android API 26 (even with desugaring enabled).
|
||||
// See https://issuetracker.google.com/issues/160484830
|
||||
'commonsIo': '2.6',
|
||||
'mime4j': '0.8.6',
|
||||
'okhttp': '4.10.0',
|
||||
'minidns': '1.0.4',
|
||||
'glide': '4.14.2',
|
||||
'jsoup': '1.15.3',
|
||||
'httpClient': '4.5.13',
|
||||
|
||||
'androidxTestRunner': '1.4.0',
|
||||
'junit': '4.13.2',
|
||||
'robolectric': '4.9',
|
||||
'mockito': '4.8.0',
|
||||
'mockitoKotlin': '4.0.0',
|
||||
'truth': '1.1.3',
|
||||
'turbine': '0.11.0',
|
||||
|
||||
'ktlint': '0.44.0'
|
||||
]
|
||||
|
||||
javaVersion = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
|
||||
classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:1.7.22-1.0.8"
|
||||
classpath "org.jlleitschuh.gradle:ktlint-gradle:11.0.0"
|
||||
}
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.android.library) apply false
|
||||
alias(libs.plugins.android.lint) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.parcelize) apply false
|
||||
alias(libs.plugins.kotlin.jvm) apply false
|
||||
alias(libs.plugins.ktlint) apply false
|
||||
}
|
||||
|
||||
project.ext {
|
||||
|
@ -80,23 +19,23 @@ project.ext {
|
|||
allprojects {
|
||||
configurations.all {
|
||||
resolutionStrategy.dependencySubstitution {
|
||||
substitute module("androidx.core:core") using module("androidx.core:core:${versions.androidxCore}")
|
||||
substitute module("androidx.activity:activity") using module("androidx.activity:activity:${versions.androidxActivity}")
|
||||
substitute module("androidx.activity:activity-ktx") using module("androidx.activity:activity-ktx:${versions.androidxActivity}")
|
||||
substitute module("androidx.fragment:fragment") using module("androidx.fragment:fragment:${versions.androidxFragment}")
|
||||
substitute module("androidx.fragment:fragment-ktx") using module("androidx.fragment:fragment-ktx:${versions.androidxFragment}")
|
||||
substitute module("androidx.appcompat:appcompat") using module("androidx.appcompat:appcompat:${versions.androidxAppCompat}")
|
||||
substitute module("androidx.preference:preference") using module("androidx.preference:preference:${versions.androidxPreference}")
|
||||
substitute module("androidx.recyclerview:recyclerview") using module("androidx.recyclerview:recyclerview:${versions.androidxRecyclerView}")
|
||||
substitute module("androidx.constraintlayout:constraintlayout") using module("androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}")
|
||||
substitute module("androidx.drawerlayout:drawerlayout") using module("androidx.drawerlayout:drawerlayout:${versions.androidxDrawerLayout}")
|
||||
substitute module("androidx.lifecycle:lifecycle-livedata") using module("androidx.lifecycle:lifecycle-livedata:${versions.androidxLifecycle}")
|
||||
substitute module("androidx.transition:transition") using module("androidx.transition:transition:${versions.androidxTransition}")
|
||||
substitute module("org.jetbrains:annotations") using module("org.jetbrains:annotations:${versions.jetbrainsAnnotations}")
|
||||
substitute module("org.jetbrains.kotlin:kotlin-stdlib") using module("org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}")
|
||||
substitute module("org.jetbrains.kotlin:kotlin-stdlib-jdk7") using module("org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}")
|
||||
substitute module("org.jetbrains.kotlin:kotlin-stdlib-jdk8") using module("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}")
|
||||
substitute module("org.jetbrains.kotlinx:kotlinx-coroutines-android") using module("org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.kotlinCoroutines}")
|
||||
substitute module("androidx.core:core") using module("androidx.core:core:${libs.versions.androidxCore.get()}")
|
||||
substitute module("androidx.activity:activity") using module("androidx.activity:activity:${libs.versions.androidxActivity.get()}")
|
||||
substitute module("androidx.activity:activity-ktx") using module("androidx.activity:activity-ktx:${libs.versions.androidxActivity.get()}")
|
||||
substitute module("androidx.fragment:fragment") using module("androidx.fragment:fragment:${libs.versions.androidxFragment.get()}")
|
||||
substitute module("androidx.fragment:fragment-ktx") using module("androidx.fragment:fragment-ktx:${libs.versions.androidxFragment.get()}")
|
||||
substitute module("androidx.appcompat:appcompat") using module("androidx.appcompat:appcompat:${libs.versions.androidxAppCompat.get()}")
|
||||
substitute module("androidx.preference:preference") using module("androidx.preference:preference:${libs.versions.androidxPreference.get()}")
|
||||
substitute module("androidx.recyclerview:recyclerview") using module("androidx.recyclerview:recyclerview:${libs.versions.androidxRecyclerView.get()}")
|
||||
substitute module("androidx.constraintlayout:constraintlayout") using module("androidx.constraintlayout:constraintlayout:${libs.versions.androidxConstraintLayout.get()}")
|
||||
substitute module("androidx.drawerlayout:drawerlayout") using module("androidx.drawerlayout:drawerlayout:${libs.versions.androidxDrawerLayout.get()}")
|
||||
substitute module("androidx.lifecycle:lifecycle-livedata") using module("androidx.lifecycle:lifecycle-livedata:${libs.versions.androidxLifecycle.get()}")
|
||||
substitute module("androidx.transition:transition") using module("androidx.transition:transition:${libs.versions.androidxTransition.get()}")
|
||||
substitute module("org.jetbrains:annotations") using module("org.jetbrains:annotations:${libs.versions.jetbrainsAnnotations.get()}")
|
||||
substitute module("org.jetbrains.kotlin:kotlin-stdlib") using module("org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}")
|
||||
substitute module("org.jetbrains.kotlin:kotlin-stdlib-jdk7") using module("org.jetbrains.kotlin:kotlin-stdlib-jdk7:${libs.versions.kotlin.get()}")
|
||||
substitute module("org.jetbrains.kotlin:kotlin-stdlib-jdk8") using module("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${libs.versions.kotlin.get()}")
|
||||
substitute module("org.jetbrains.kotlinx:kotlinx-coroutines-android") using module("org.jetbrains.kotlinx:kotlinx-coroutines-android:${libs.versions.kotlinCoroutines.get()}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,8 +52,8 @@ allprojects {
|
|||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility javaVersion
|
||||
targetCompatibility javaVersion
|
||||
sourceCompatibility libs.versions.java.get()
|
||||
targetCompatibility libs.versions.java.get()
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
|
@ -132,8 +71,8 @@ allprojects {
|
|||
|
||||
plugins.withType(JavaPlugin).configureEach {
|
||||
project.java {
|
||||
sourceCompatibility = javaVersion
|
||||
targetCompatibility = javaVersion
|
||||
sourceCompatibility = libs.versions.java.get()
|
||||
targetCompatibility = libs.versions.java.get()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,13 +87,13 @@ allprojects {
|
|||
|
||||
tasks.withType(KotlinCompile) {
|
||||
kotlinOptions {
|
||||
jvmTarget = javaVersion
|
||||
jvmTarget = libs.versions.java.get()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'org.jlleitschuh.gradle.ktlint'
|
||||
ktlint {
|
||||
version = versions.ktlint
|
||||
version = libs.versions.ktlint.get()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
10
fastlane/metadata/android/en-US/changelogs/35000.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/35000.txt
Normal 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
|
3
fastlane/metadata/android/en-US/changelogs/35001.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/35001.txt
Normal 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
|
1
fastlane/metadata/android/en-US/changelogs/35002.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/35002.txt
Normal file
|
@ -0,0 +1 @@
|
|||
- Fixed crash at app startup
|
125
gradle/libs.versions.toml
Normal file
125
gradle/libs.versions.toml
Normal 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"
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue