From 9800f8e9336a9d8940fbda5e2d04e0a59b1b33f8 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 12:14:05 +0200 Subject: [PATCH 01/85] Prepare for version 6.302 --- app/k9mail/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 0c2b5c96be..8e701962a0 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -49,7 +49,7 @@ android { testApplicationId "com.fsck.k9.tests" versionCode 33001 - versionName '6.301' + versionName '6.302-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", -- GitLab From da01b7a5c8610fa7e47adfc26e7cae9232dfcbc2 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 13:01:50 +0200 Subject: [PATCH 02/85] Update Kotlin compiler options --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2e70b964e8..1e6d396e76 100644 --- a/build.gradle +++ b/build.gradle @@ -104,7 +104,7 @@ subprojects { tasks.withType(KotlinCompile) { kotlinOptions { - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } } -- GitLab From 9328bd5dc73be9e9b0a2404f8c92fe64d667d710 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 13:37:31 +0200 Subject: [PATCH 03/85] Switch from using `declaringClass` to `declaringJavaClass` --- .../src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt index 38e7ccd5a0..e9b8ffe31a 100644 --- a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt +++ b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt @@ -521,11 +521,11 @@ class AccountPreferenceSerializer( defaultEnum } else { try { - java.lang.Enum.valueOf(defaultEnum.declaringClass, stringPref) + java.lang.Enum.valueOf(defaultEnum.declaringJavaClass, stringPref) } catch (ex: IllegalArgumentException) { Timber.w( ex, "Unable to convert preference key [%s] value [%s] to enum of type %s", - key, stringPref, defaultEnum.declaringClass + key, stringPref, defaultEnum.declaringJavaClass ) defaultEnum -- GitLab From a5c5278c71c98a8af366769f22dd5b549e40fb51 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 13:55:01 +0200 Subject: [PATCH 04/85] Add 'com.android.lint' Gradle plugin to JVM projects --- app/autodiscovery/api/build.gradle | 1 + app/autodiscovery/srvrecords/build.gradle | 1 + app/autodiscovery/thunderbird/build.gradle | 1 + app/html-cleaner/build.gradle | 1 + mail/common/build.gradle | 1 + mail/protocols/imap/build.gradle | 1 + mail/protocols/pop3/build.gradle | 1 + mail/protocols/smtp/build.gradle | 1 + mail/protocols/webdav/build.gradle | 1 + mail/testing/build.gradle | 1 + 10 files changed, 10 insertions(+) diff --git a/app/autodiscovery/api/build.gradle b/app/autodiscovery/api/build.gradle index 55914b0cc0..ce98344e01 100644 --- a/app/autodiscovery/api/build.gradle +++ b/app/autodiscovery/api/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'java-library' apply plugin: 'kotlin' +apply plugin: 'com.android.lint' java { sourceCompatibility = javaVersion diff --git a/app/autodiscovery/srvrecords/build.gradle b/app/autodiscovery/srvrecords/build.gradle index 36a5976850..a0574af319 100644 --- a/app/autodiscovery/srvrecords/build.gradle +++ b/app/autodiscovery/srvrecords/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'java-library' apply plugin: 'kotlin' +apply plugin: 'com.android.lint' java { sourceCompatibility = javaVersion diff --git a/app/autodiscovery/thunderbird/build.gradle b/app/autodiscovery/thunderbird/build.gradle index dc48d2c50f..8e28048489 100644 --- a/app/autodiscovery/thunderbird/build.gradle +++ b/app/autodiscovery/thunderbird/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'java-library' apply plugin: 'kotlin' +apply plugin: 'com.android.lint' java { sourceCompatibility = javaVersion diff --git a/app/html-cleaner/build.gradle b/app/html-cleaner/build.gradle index eff0c3492f..64fa4b0880 100644 --- a/app/html-cleaner/build.gradle +++ b/app/html-cleaner/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'java-library' apply plugin: 'kotlin' +apply plugin: 'com.android.lint' java { sourceCompatibility = javaVersion diff --git a/mail/common/build.gradle b/mail/common/build.gradle index bf282669e6..7b4ad7508d 100644 --- a/mail/common/build.gradle +++ b/mail/common/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'java-library' apply plugin: 'kotlin' +apply plugin: 'com.android.lint' if (rootProject.testCoverage) { apply plugin: 'jacoco' diff --git a/mail/protocols/imap/build.gradle b/mail/protocols/imap/build.gradle index f86c05f47f..3150d7d9a8 100644 --- a/mail/protocols/imap/build.gradle +++ b/mail/protocols/imap/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'java-library' apply plugin: 'kotlin' +apply plugin: 'com.android.lint' if (rootProject.testCoverage) { apply plugin: 'jacoco' diff --git a/mail/protocols/pop3/build.gradle b/mail/protocols/pop3/build.gradle index 8f750863e6..f798dc6667 100644 --- a/mail/protocols/pop3/build.gradle +++ b/mail/protocols/pop3/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'java-library' +apply plugin: 'com.android.lint' if (rootProject.testCoverage) { apply plugin: 'jacoco' diff --git a/mail/protocols/smtp/build.gradle b/mail/protocols/smtp/build.gradle index fde2b46a9d..e7740641e7 100644 --- a/mail/protocols/smtp/build.gradle +++ b/mail/protocols/smtp/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'java-library' apply plugin: 'kotlin' +apply plugin: 'com.android.lint' if (rootProject.testCoverage) { apply plugin: 'jacoco' diff --git a/mail/protocols/webdav/build.gradle b/mail/protocols/webdav/build.gradle index f7af9dcd20..49b4e08bd1 100644 --- a/mail/protocols/webdav/build.gradle +++ b/mail/protocols/webdav/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'java-library' +apply plugin: 'com.android.lint' if (rootProject.testCoverage) { apply plugin: 'jacoco' diff --git a/mail/testing/build.gradle b/mail/testing/build.gradle index c1d44d29a5..709cd1c9aa 100644 --- a/mail/testing/build.gradle +++ b/mail/testing/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'java-library' apply plugin: 'kotlin' +apply plugin: 'com.android.lint' if (rootProject.testCoverage) { apply plugin: 'jacoco' -- GitLab From 62b7051e30ac659125bedcf7c5b2041482519d93 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 15:59:58 +0200 Subject: [PATCH 05/85] Add position information to format string parameters --- app/ui/legacy/src/main/res/values/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml index d6fb93cff4..5ba8fc72fe 100644 --- a/app/ui/legacy/src/main/res/values/strings.xml +++ b/app/ui/legacy/src/main/res/values/strings.xml @@ -182,7 +182,7 @@ Please submit bug reports, contribute new features and ask questions at %d new message %d new messages - %d Unread (%s) + %1$d Unread (%2$s) + %1$d more on %2$s Reply @@ -205,7 +205,7 @@ Please submit bug reports, contribute new features and ask questions at An error has occurred while trying to create a system notification for a new message. The reason is most likely a missing notification sound.\n\nTap to open notification settings. - Checking mail: %s:%s + Checking mail: %1$s:%2$s Checking mail Sending mail: %s Sending mail @@ -259,7 +259,7 @@ Please submit bug reports, contribute new features and ask questions at Edit quoted text Remove attachment - From: %s <%s> + From: %1$s <%2$s> To: Cc: Bcc: -- GitLab From 867733c25d9da8f13dcba9f6e844f4e7b3b00c2e Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:02:01 +0200 Subject: [PATCH 06/85] Update translations --- app/ui/legacy/src/main/res/values-ar/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-be/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-bg/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-br/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-ca/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-cs/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-cy/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-da/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-de/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-el/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-eo/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-es/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-et/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-eu/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-fa/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-fi/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-fr/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-fy/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-gd/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-gl-rES/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-gl/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-hr/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-hu/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-in/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-is/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-it/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-iw/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-ja/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-ko/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-lt/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-lv/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-ml/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-nb/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-nl/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-pl/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-pt-rBR/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-pt-rPT/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-ro/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-ru/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-sk/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-sl/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-sq/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-sr/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-sv/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-tr/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-uk/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-zh-rCN/strings.xml | 6 +++--- app/ui/legacy/src/main/res/values-zh-rTW/strings.xml | 6 +++--- 48 files changed, 144 insertions(+), 144 deletions(-) diff --git a/app/ui/legacy/src/main/res/values-ar/strings.xml b/app/ui/legacy/src/main/res/values-ar/strings.xml index 6f34880a73..bc50f68e22 100644 --- a/app/ui/legacy/src/main/res/values-ar/strings.xml +++ b/app/ui/legacy/src/main/res/values-ar/strings.xml @@ -147,7 +147,7 @@ %d رسالة %d رسالة جديدة - %d غير مقروئة (%s) + %1$d غير مقروئة (%2$s) رُد إعتبارها مقروءة إعتبار الكل كمقروء @@ -162,7 +162,7 @@ فشل مصادقة %s. يرجى تحديث إعدادات الخادم. - تفقد البريد: %s:%s + تفقد البريد: %1$s:%2$s التحقق من البريد إرسال البريد: %s إرسال البريد @@ -203,7 +203,7 @@ حذف النص المقتبَس تعديل النص المقتبَس إزالة المُرفَق - من: %s <%s> + من: %1$s <%2$s> إلى : نسخة كربونية : نسخة مخفية: diff --git a/app/ui/legacy/src/main/res/values-be/strings.xml b/app/ui/legacy/src/main/res/values-be/strings.xml index db88ba087a..429a82aaf1 100644 --- a/app/ui/legacy/src/main/res/values-be/strings.xml +++ b/app/ui/legacy/src/main/res/values-be/strings.xml @@ -145,7 +145,7 @@ K-9 Mail - шматфункцыянальны свабодны паштовы к %d новых лістоў %d новых лістоў - %d новых (%s) + %1$d новых (%2$s) + %1$d яшчэ ў %2$s Адказаць Пазначыць прачытаным @@ -161,7 +161,7 @@ K-9 Mail - шматфункцыянальны свабодны паштовы к Няўдалая аўтарызацыя для %s. Змяніце налады сервера. - Праверка пошты: %s:%s + Праверка пошты: %1$s:%2$s Праверка пошты Адпраўка пошты: %s Адпраўленне пошты @@ -203,7 +203,7 @@ K-9 Mail - шматфункцыянальны свабодны паштовы к Выдаліць цытату Рэдагаваць цытату Выдаліць далучаныя файлы - Ад: %s <%s> + Ад: %1$s <%2$s> Каму: Копія: Схаваная копія: diff --git a/app/ui/legacy/src/main/res/values-bg/strings.xml b/app/ui/legacy/src/main/res/values-bg/strings.xml index 85a3ee775d..e91d059336 100644 --- a/app/ui/legacy/src/main/res/values-bg/strings.xml +++ b/app/ui/legacy/src/main/res/values-bg/strings.xml @@ -148,7 +148,7 @@ K-9 Mail е мощен, безплатен имейл клиент за Андр %d ново съобщение %d нови съобщения - %d Непрочетени (%s) + %1$d Непрочетени (%2$s) + %1$d в %2$s Отговори Маркирай като прочетено @@ -165,7 +165,7 @@ K-9 Mail е мощен, безплатен имейл клиент за Андр Идентификацията за %s е неуспешна. Обновете сървърните настройки. - Проверка на поща: %s:%s + Проверка на поща: %1$s:%2$s Проверка на поща Изпраща писмо: %s Изпраща писмо @@ -209,7 +209,7 @@ K-9 Mail е мощен, безплатен имейл клиент за Андр Премахни цитираният текст Редактирай цитираният текст Премахни прикачени файлове - От: %s <%s> + От: %1$s <%2$s> До: Копие: Bcc: diff --git a/app/ui/legacy/src/main/res/values-br/strings.xml b/app/ui/legacy/src/main/res/values-br/strings.xml index 2abaf87a9d..1209c736f1 100644 --- a/app/ui/legacy/src/main/res/values-br/strings.xml +++ b/app/ui/legacy/src/main/res/values-br/strings.xml @@ -133,7 +133,7 @@ Danevellit beugoù, kenlabourit war keweriusterioù nevez ha savit goulennoù wa %d a gemennadennoù nevez %d a gemennadennoù nevez - %d Anlennet (%s) + %1$d Anlennet (%2$s) + %1$d ouzhpenn war %2$s Respont Merkañ evel lennet @@ -149,7 +149,7 @@ Danevellit beugoù, kenlabourit war keweriusterioù nevez ha savit goulennoù wa C’hwitadenn war an dilesa evit %s. Hizivait an arventennoù dafariad. - O kerc’hat ar posteloù: %s:%s + O kerc’hat ar posteloù: %1$s:%2$s Kerc’hat ar posteloù O kas ar postel: %s O kas ar postel @@ -190,7 +190,7 @@ Danevellit beugoù, kenlabourit war keweriusterioù nevez ha savit goulennoù wa Enkorfañ ar gemennadenn meneget Dilemel an destenn meneget Embann an destenn meneget - A-berzh: %s &amp;lt;%s&amp;gt; + A-berzh: %1$s <%2$s> Da: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-ca/strings.xml b/app/ui/legacy/src/main/res/values-ca/strings.xml index c28db5e2c9..e0a179622f 100644 --- a/app/ui/legacy/src/main/res/values-ca/strings.xml +++ b/app/ui/legacy/src/main/res/values-ca/strings.xml @@ -153,7 +153,7 @@ Si us plau, envieu informes d\'errors, contribuïu-hi amb noves millores i feu p %d nous missatges %d nous missatges - %d no llegit(s) (%s) + %1$d no llegit(s) (%2$s) + %1$d més sobre %2$s Respon Marca com a llegit @@ -172,7 +172,7 @@ Si us plau, envieu informes d\'errors, contribuïu-hi amb noves millores i feu p Error de notificació S\'ha produït un error en intentar crear una notificació del sistema per a un missatge nou. El motiu més probable és que falti un so de notificació.\n\nToqueu per obrir la configuració de notificació. - S\'està comprovant el correu: %s:%s + S\'està comprovant el correu: %1$s:%2$s S\'està comprovant el correu S\'està enviant correu: %s S\'està enviant correu @@ -219,7 +219,7 @@ Si us plau, envieu informes d\'errors, contribuïu-hi amb noves millores i feu p Elimina el text citat Edita el text citat Suprimeix l\'adjunt - De: %s <%s> + De: %1$s <%2$s> A: A/c: Bcc: diff --git a/app/ui/legacy/src/main/res/values-cs/strings.xml b/app/ui/legacy/src/main/res/values-cs/strings.xml index f998146ee5..f5303c07ac 100644 --- a/app/ui/legacy/src/main/res/values-cs/strings.xml +++ b/app/ui/legacy/src/main/res/values-cs/strings.xml @@ -157,7 +157,7 @@ Hlášení o chyb, úpravy pro nové funkce a dotazy zadávejte prostřednictví %d nových zpráv %d nové zprávy - %d Nepřečteno (%s) + %1$d Nepřečteno (%2$s) + %1$d více na %2$s Odpovědět Přečteno @@ -176,7 +176,7 @@ Hlášení o chyb, úpravy pro nové funkce a dotazy zadávejte prostřednictví Chyba upozornění Vyskytla se chyba při pokusu o vytvoření systémového upozornění pro novou zprávu. Důvodem je nejspíš chybějící zvuk upozornění.\n\nKlepnutím otevřete nastavení upozorňování. - Zjišťování pošty: %s:%s + Zjišťování pošty: %1$s:%2$s Zjišťování pošty Odesílání pošty: %s Odesílání pošty @@ -223,7 +223,7 @@ Hlášení o chyb, úpravy pro nové funkce a dotazy zadávejte prostřednictví Odstranit citovaný text Upravit citovaný text Odebrat přílohu - Odesílatel: %s <%s> + Odesílatel: %1$s <%2$s> Komu: Kopie: Skrytá kopie (Bcc): diff --git a/app/ui/legacy/src/main/res/values-cy/strings.xml b/app/ui/legacy/src/main/res/values-cy/strings.xml index 5116b0e7ed..8d54430916 100644 --- a/app/ui/legacy/src/main/res/values-cy/strings.xml +++ b/app/ui/legacy/src/main/res/values-cy/strings.xml @@ -155,7 +155,7 @@ Plîs rho wybod am unrhyw wallau, syniadau am nodweddion newydd, neu ofyn cwesti %d neges newydd %d neges newydd - %d Heb eu darllen (%s) + %1$d Heb eu darllen (%2$s) + %1$d yn rhagor ar %2$s Ateb Nodi wedi ei Darllen @@ -172,7 +172,7 @@ Plîs rho wybod am unrhyw wallau, syniadau am nodweddion newydd, neu ofyn cwesti Methodd y dilysiad ar gyfer %s. Diweddara dy osodiadau gweinydd. - Yn gwirio am negeseuon: %s:%s + Yn gwirio am negeseuon: %1$s:%2$s Yn gwirio am negeseuon Yn anfon negeseuon: %s Yn anfon neges @@ -219,7 +219,7 @@ Plîs rho wybod am unrhyw wallau, syniadau am nodweddion newydd, neu ofyn cwesti Tynnu\'r testun wedi ei ddyfynnu Golygu\'r testun wedi ei ddyfynnu Tynnu atodiad - Oddi wrth: %s <%s> + Oddi wrth: %1$s <%2$s> At: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-da/strings.xml b/app/ui/legacy/src/main/res/values-da/strings.xml index ce9a14db47..e4c3ecb9f7 100644 --- a/app/ui/legacy/src/main/res/values-da/strings.xml +++ b/app/ui/legacy/src/main/res/values-da/strings.xml @@ -150,7 +150,7 @@ Rapporter venligst fejl, forslag til nye funktioner eller stil spørgsmål på: %dnyt brev %d nye meddelelser - %d ulæst(e) (%s) + %1$d ulæst(e) (%2$s) + %1$d mere på %2$s Svar Markér som læst @@ -167,7 +167,7 @@ Rapporter venligst fejl, forslag til nye funktioner eller stil spørgsmål på: Godkendelse mislykkedes for %s. Opdater dine serverindstillinger. - Synkroniserer mail: %s:%s + Synkroniserer mail: %1$s:%2$s Kontrollere post Sender mail: %s Sender post @@ -213,7 +213,7 @@ Rapporter venligst fejl, forslag til nye funktioner eller stil spørgsmål på: Fjerne citeret tekst Redigere citeret tekst Fjern vedhæftning - Fra: %s <%s> + Fra: %1$s <%2$s> Til: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-de/strings.xml b/app/ui/legacy/src/main/res/values-de/strings.xml index 0ec38c7c78..8862df3513 100644 --- a/app/ui/legacy/src/main/res/values-de/strings.xml +++ b/app/ui/legacy/src/main/res/values-de/strings.xml @@ -152,7 +152,7 @@ Bitte senden Sie Fehlerberichte, Ideen für neue Funktionen und stellen Sie Frag %d neue Nachricht %d neue Nachrichten - %d Ungelesen (%s) + %1$d Ungelesen (%2$s) + %1$d weitere auf %2$s Antworten Gelesen @@ -171,7 +171,7 @@ Bitte senden Sie Fehlerberichte, Ideen für neue Funktionen und stellen Sie Frag Benachrichtigungsfehler Beim Versuch, eine Systembenachrichtigung für eine neue Nachricht zu erstellen, ist ein Fehler aufgetreten. Der Grund dafür ist höchstwahrscheinlich ein fehlender Benachrichtigungston.\n\nAntippen, um die Benachrichtigungseinstellungen zu öffnen. - Neue E-Mails in %s:%s werden abgerufen + Neue E-Mails in %1$s:%2$s werden abgerufen E-Mails werden abgerufen E-Mail in %s wird gesendet E-Mail wird gesendet @@ -218,7 +218,7 @@ Bitte senden Sie Fehlerberichte, Ideen für neue Funktionen und stellen Sie Frag Zitierten Text entfernen Zitierten Text bearbeiten Anhang entfernen - Von: %s <%s> + Von: %1$s <%2$s> An: CC: BCC: diff --git a/app/ui/legacy/src/main/res/values-el/strings.xml b/app/ui/legacy/src/main/res/values-el/strings.xml index 4540bddf3d..1df7496674 100644 --- a/app/ui/legacy/src/main/res/values-el/strings.xml +++ b/app/ui/legacy/src/main/res/values-el/strings.xml @@ -148,7 +148,7 @@ %d νέο μήνυμα %d νέα μηνύματα - %d μη αναγνωσμένα (%s) + %1$d μη αναγνωσμένα (%2$s) + %1$d περισσότερα για %2$s Απάντηση Ανάγνωση @@ -164,7 +164,7 @@ Αποτυχία πιστοποίησης %s. Ενημερώστε τις ρυθμίσεις του εξυπηρετητή σας. - Έλεγχος μηνύματος: %s:%s + Έλεγχος μηνύματος: %1$s:%2$s Έλεγχος μηνύματος Αποστολή μηνύματος: %s Αποστολή μηνύματος @@ -208,7 +208,7 @@ Αφαίρεση κειμένου προηγ. μηνύματος Αλλαγή κειμένου προηγ. μηνύματος Αφαίρεση συνημμένου - Από: %s <%s> + Από: %1$s <%2$s> Προς: Κοιν: Κρυφή Κοιν: diff --git a/app/ui/legacy/src/main/res/values-eo/strings.xml b/app/ui/legacy/src/main/res/values-eo/strings.xml index 2c0f23711b..c33bed3792 100644 --- a/app/ui/legacy/src/main/res/values-eo/strings.xml +++ b/app/ui/legacy/src/main/res/values-eo/strings.xml @@ -147,7 +147,7 @@ Bonvolu raporti erarojn, kontribui novajn eblojn kaj peti pri novaj funkcioj per %d nova mesaĝo %d novaj mesaĝoj - %d novaj (%s) + %1$d novaj (%2$s) + %1$d pliaj en %2$s Respondi Marki kiel legitan @@ -163,7 +163,7 @@ Bonvolu raporti erarojn, kontribui novajn eblojn kaj peti pri novaj funkcioj per Aŭtentigo por %s malsukcesis. Aktualigu agordojn de servilo. - Kontrolado de retpoŝto: %s:%s + Kontrolado de retpoŝto: %1$s:%2$s Kontrolado de retpoŝto Sendado de retletero: %s Sendado de retletero @@ -207,7 +207,7 @@ Bonvolu raporti erarojn, kontribui novajn eblojn kaj peti pri novaj funkcioj per Forigi citatan tekston Redakti citatan tekston Forigi kunsendaĵon - De: %s <%s> + De: %1$s <%2$s> Al: Kopio: Kaŝkopio: diff --git a/app/ui/legacy/src/main/res/values-es/strings.xml b/app/ui/legacy/src/main/res/values-es/strings.xml index 1e8241f2c0..557a2e1769 100644 --- a/app/ui/legacy/src/main/res/values-es/strings.xml +++ b/app/ui/legacy/src/main/res/values-es/strings.xml @@ -153,7 +153,7 @@ Puedes informar de fallos, contribuir con su desarrollo y hacer preguntas en %d mensajes nuevos %d mensajes nuevos - %d Sin leer (%s) + %1$d Sin leer (%2$s) + %1$d más en %2$s Responder Marcar como leído @@ -172,7 +172,7 @@ Puedes informar de fallos, contribuir con su desarrollo y hacer preguntas en Notificación fallida Ha ocurrido un problema al intentar crear una notificación del sistema alertando de un correo nuevo. Seguramente sea por no tener un sonido de alerta puesto.\n\nToca para ver los ajustes de notificaciones. - Comprobando correo: %s:%s + Comprobando correo: %1$s:%2$s Comprobando correo Enviando correo: %s Enviando correo @@ -219,7 +219,7 @@ Puedes informar de fallos, contribuir con su desarrollo y hacer preguntas en No incluir el mensaje original Editar el mensaje original Eliminar adjunto - De: %s <%s> + De: %1$s <%2$s> Para: Cc: Cco: diff --git a/app/ui/legacy/src/main/res/values-et/strings.xml b/app/ui/legacy/src/main/res/values-et/strings.xml index eddc5ec0b8..f27f31c26d 100644 --- a/app/ui/legacy/src/main/res/values-et/strings.xml +++ b/app/ui/legacy/src/main/res/values-et/strings.xml @@ -154,7 +154,7 @@ Veateated saad saata, kaastööd teha ning küsida teavet järgmisel lehel: %d uus kiri %d uut kirja - %d Lugemata (%s) + %1$d Lugemata (%2$s) + %1$d veel kontol %2$s Vasta Märgi loetuks @@ -173,7 +173,7 @@ Veateated saad saata, kaastööd teha ning küsida teavet järgmisel lehel: Viga teavitusel Uue saabunud sõnumi kohta süsteemise teavituse loomisel tekkis viga. Üks võimalik põhjus on teavitusele määratud heli puudumine.\n\nAva teavituste seadistused. - Kontrollib e-kirju: %s:%s + Kontrollib e-kirju: %1$s:%2$s Kontrollib e-kirju Saadab e-kirja: %s Saadab e-kirja @@ -220,7 +220,7 @@ Veateated saad saata, kaastööd teha ning küsida teavet järgmisel lehel: Eemalda osundatud tekst Muuda osundatud teksti Eemalda manus - Kellelt: %s <%s> + Kellelt: %1$s <%2$s> Kellele: Koopia: Pimekoopia: diff --git a/app/ui/legacy/src/main/res/values-eu/strings.xml b/app/ui/legacy/src/main/res/values-eu/strings.xml index f1ec34ef5d..edcd105d6e 100644 --- a/app/ui/legacy/src/main/res/values-eu/strings.xml +++ b/app/ui/legacy/src/main/res/values-eu/strings.xml @@ -146,7 +146,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Mezu berri %d %d mezu berri - %d irakurri gabeko (%s) + %1$d irakurri gabeko (%2$s) + %1$d gehiago %2$s-(e)n Erantzun Markatu irakurritako gisa @@ -162,7 +162,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Autentifikazioak huts egin du %s-(e)rako. Eguneratu zerbitzariaren ezarpenak. - Posta egiaztatzen: %s:%s + Posta egiaztatzen: %1$s:%2$s Posta egiaztatzen Posta bidaltzen: %s Posta bidaltzea @@ -206,7 +206,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Kendu aipua Editatu aipua Kendu eranskina - Nondik: %s <%s> + Nondik: %1$s <%2$s> Nori: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-fa/strings.xml b/app/ui/legacy/src/main/res/values-fa/strings.xml index 4778ea848e..86b5caa411 100644 --- a/app/ui/legacy/src/main/res/values-fa/strings.xml +++ b/app/ui/legacy/src/main/res/values-fa/strings.xml @@ -150,7 +150,7 @@ %d نامهٔ جدید %d پیام جدید - %d نخوانده (%s) + %1$d نخوانده (%2$s) + %1$d تای دیگر در %2$s پاسخ خوانده شد @@ -167,7 +167,7 @@ اعتبارسنجی %s ناموفق بود. تنظیمات کارساز خود را به‌روز کنید. - به‌روزآوری رایانامه‌ها: %s:%s + به‌روزآوری رایانامه‌ها: %1$s:%2$s به‌روزآوری رایانامه‌ها ارسال رایانامه: %s ارسال رایانامه @@ -214,7 +214,7 @@ حذف نقل‌قول ویرایش نقل‌قول حذف پیوست - از: %s <%s> + از: %1$s <%2$s> به: رونوشت به: مخفیانه به: diff --git a/app/ui/legacy/src/main/res/values-fi/strings.xml b/app/ui/legacy/src/main/res/values-fi/strings.xml index 15d34f76b9..5277c42495 100644 --- a/app/ui/legacy/src/main/res/values-fi/strings.xml +++ b/app/ui/legacy/src/main/res/values-fi/strings.xml @@ -152,7 +152,7 @@ Ilmoita virheistä, ota osaa sovelluskehitykseen ja esitä kysymyksiä osoittees %d uusi viesti %d uutta viestiä - %d lukematonta (%s) + %1$d lukematonta (%2$s) + %1$d lisää tilillä %2$s Vastaa Merkitse luetuksi @@ -171,7 +171,7 @@ Ilmoita virheistä, ota osaa sovelluskehitykseen ja esitä kysymyksiä osoittees Ilmoitusvirhe Uuteen viestiin liittyvää järjestelmäilmoitusta luotaessa tapahtui virhe. Syy on mitä luultavimmin puuttuva ilmoitusääni.\n\nNapauta avataksesi ilmoitusasetukset. - Tarkistetaan viestejä: %s:%s + Tarkistetaan viestejä: %1$s:%2$s Tarkistetaan viestejä Lähetetään viestejä: %s Lähetetään viestejä @@ -218,7 +218,7 @@ Ilmoita virheistä, ota osaa sovelluskehitykseen ja esitä kysymyksiä osoittees Poista lainattu teksti Muokkaa lainattua tekstiä Poista liite - Lähettäjä: %s <%s> + Lähettäjä: %1$s <%2$s> Vast.ottaja: Kopio: Piilokopio: diff --git a/app/ui/legacy/src/main/res/values-fr/strings.xml b/app/ui/legacy/src/main/res/values-fr/strings.xml index 4aeb628f93..eb7a2be1b6 100644 --- a/app/ui/legacy/src/main/res/values-fr/strings.xml +++ b/app/ui/legacy/src/main/res/values-fr/strings.xml @@ -156,7 +156,7 @@ Rapportez les bogues, recommandez de nouvelles fonctions et posez vos questions %d nouveaux courriels %d nouveaux courriels - %d non lus (%s) + %1$d non lus (%2$s) + %1$d de plus sur %2$s Répondre Marquer comme lu @@ -175,7 +175,7 @@ Rapportez les bogues, recommandez de nouvelles fonctions et posez vos questions Erreur de notification Impossible de créer une notification système de nouveau message. Un son de notification manquant en est probablement la cause.\n\nToucher pour ouvrir les paramètres de notification. - Relève des courriels : %s:%s + Relève des courriels : %1$s:%2$s Relève des courriels Envoi du courriel : %s Envoi du courriel @@ -222,7 +222,7 @@ Rapportez les bogues, recommandez de nouvelles fonctions et posez vos questions Supprimer le texte entre guillemets Modifier le texte entre guillemets Supprimer le fichier joint - De : %s <%s> + De : %1$s <%2$s> À : Cc : Cci : diff --git a/app/ui/legacy/src/main/res/values-fy/strings.xml b/app/ui/legacy/src/main/res/values-fy/strings.xml index 3f42c9df78..b28849f4ec 100644 --- a/app/ui/legacy/src/main/res/values-fy/strings.xml +++ b/app/ui/legacy/src/main/res/values-fy/strings.xml @@ -148,7 +148,7 @@ Graach flaterrapporten stjoere, bydragen foar nije funksjes en fragen stelle op %d nije berjocht %d nije berjochten - %d Net lêzen (%s) + %1$d Net lêzen (%2$s) + %1$d mear by %2$s Beäntwurdzje As lêzen markearje @@ -167,7 +167,7 @@ Graach flaterrapporten stjoere, bydragen foar nije funksjes en fragen stelle op Meldingsflater Der is in flater bard wylst it meitsje fan in systeemmelding foar in nij berjocht. De reden is wierskynlik in ûntbrekken fan in meldingslûd.\n\nTik om meldingsynstellingen te iepenjen. - Berjochten kontrolearje: %s:%s + Berjochten kontrolearje: %1$s:%2$s Berjochten kontrolearje Berjochten ferstjoere: %s Berjochten ferstjoere @@ -214,7 +214,7 @@ Graach flaterrapporten stjoere, bydragen foar nije funksjes en fragen stelle op Sitaattekst fuortsmite Sitaattekst bewurkje Bylage fuortsmite - Fan: %s <%s> + Fan: %1$s <%2$s> Oan: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-gd/strings.xml b/app/ui/legacy/src/main/res/values-gd/strings.xml index a862afd95f..1b3c63b25d 100644 --- a/app/ui/legacy/src/main/res/values-gd/strings.xml +++ b/app/ui/legacy/src/main/res/values-gd/strings.xml @@ -119,7 +119,7 @@ %d teachdaireachdan ùra %d teachdaireachd ùr - %d gun leughadh (%s) + %1$d gun leughadh (%2$s) %1$d a bharrachd air %2$s Freagair Comharraich gun deach a leughadh @@ -135,7 +135,7 @@ Dh’fhàillig dearbhadh a’ chunntais %s. Ùraich roghainnean an fhrithealaiche agad. - A’ toirt sùil airson post: %s:%s + A’ toirt sùil airson post: %1$s:%2$s A’ toirt sùil airson post A’ cur a’ phuist: %s A’ cur a’ phuist @@ -175,7 +175,7 @@ Thoir an às-earrann air falbh Deasaich an às-earrann Thoir an ceanglachan air falbh - O: %s <%s> + O: %1$s <%2$s> Gu: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-gl-rES/strings.xml b/app/ui/legacy/src/main/res/values-gl-rES/strings.xml index a19b136d35..3b3d1436b1 100644 --- a/app/ui/legacy/src/main/res/values-gl-rES/strings.xml +++ b/app/ui/legacy/src/main/res/values-gl-rES/strings.xml @@ -77,7 +77,7 @@ Cargar %d máis Correo novo - %d sen ler (%s) + %1$d sen ler (%2$s) + %1$d máis en %2$s Responder Marcar como lida @@ -91,7 +91,7 @@ Comproba a configuración do servidor - Comprobando correo: %s:%s + Comprobando correo: %1$s:%2$s Comprobando correo Enviando correo: %s Enviando correo @@ -126,7 +126,7 @@ Algúns anexos non poden reenviarse porque non foron descargados previamente. Eliminar texto citado Editar texto citado - Desde: %s <%s> + Desde: %1$s <%2$s> Para: Cc: Mostrar imaxes diff --git a/app/ui/legacy/src/main/res/values-gl/strings.xml b/app/ui/legacy/src/main/res/values-gl/strings.xml index 1c1f9dc7b9..c7bf0b8df5 100644 --- a/app/ui/legacy/src/main/res/values-gl/strings.xml +++ b/app/ui/legacy/src/main/res/values-gl/strings.xml @@ -149,7 +149,7 @@ Por favor envíen informes de fallos, contribúa con novas características e co %d nova mensaxe %d novas mensaxes - %d Sin Ler (%s) + %1$d Sin Ler (%2$s) + %1$d máis en %2$s Responder Marcar Lido @@ -166,7 +166,7 @@ Por favor envíen informes de fallos, contribúa con novas características e co Fallou a autenticación para %s. Actualice os axustes do servidor. - Comprobando correo: %s:%s + Comprobando correo: %1$s:%2$s Comprobando correo Enviando correo: %s Enviando correo @@ -213,7 +213,7 @@ Por favor envíen informes de fallos, contribúa con novas características e co Eliminar texto citado Editar texto citado Eliminar anexo - Dende: %s <%s> + Dende: %1$s <%2$s> Para: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-hr/strings.xml b/app/ui/legacy/src/main/res/values-hr/strings.xml index d3da56b326..080d0d50ab 100644 --- a/app/ui/legacy/src/main/res/values-hr/strings.xml +++ b/app/ui/legacy/src/main/res/values-hr/strings.xml @@ -105,7 +105,7 @@ %d novih poruka %d novih poruka - %d Nepročitano (%s) + %1$d Nepročitano (%2$s) + %1$d više na %2$s Odgovori Označi Pročitanim @@ -121,7 +121,7 @@ Identifikacija nije uspjela za %s. Ažurirajte vaše postavke servera. - Provjeravam poštu: %s:%s + Provjeravam poštu: %1$s:%2$s Provjeravanje pošte Šaljem poštu: %s Slanje pošte @@ -163,7 +163,7 @@ Maknuti citirani tekst Urediti citirani tekst Ukloni privitak - Od: %s <%s> + Od: %1$s <%2$s> Za: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-hu/strings.xml b/app/ui/legacy/src/main/res/values-hu/strings.xml index fa50446e9a..85f2362a40 100644 --- a/app/ui/legacy/src/main/res/values-hu/strings.xml +++ b/app/ui/legacy/src/main/res/values-hu/strings.xml @@ -147,7 +147,7 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd %d új üzenet %d új üzenet - %d Olvasatlan (%s) + %1$d Olvasatlan (%2$s) + %1$d további: %2$s Válasz Megjelölés olvasottként @@ -163,7 +163,7 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd A hitelesítés sikertelen: %s. Frissítse a kiszolgálóbeállításokat. - Levelek ellenőrzése: %s:%s + Levelek ellenőrzése: %1$s:%2$s Levelek ellenőrzése Levél küldése: %s Levél küldése @@ -207,7 +207,7 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd Idézett szöveg eltávolítása Idézett szöveg szerkesztése Melléklet eltávolítása - Feladó: %s <%s> + Feladó: %1$s <%2$s> Címzett: Másolat: Titkos másolat: diff --git a/app/ui/legacy/src/main/res/values-in/strings.xml b/app/ui/legacy/src/main/res/values-in/strings.xml index db82a65874..46da466ebc 100644 --- a/app/ui/legacy/src/main/res/values-in/strings.xml +++ b/app/ui/legacy/src/main/res/values-in/strings.xml @@ -140,7 +140,7 @@ Kirimkan laporan bug, kontribusikan fitur baru dan ajukan pertanyaan di %d pesan baru - %d Belum dibaca (%s) + %1$d Belum dibaca (%2$s) + %1$d lagi pada %2$s Balas Tandai Sudah Dibaca @@ -156,7 +156,7 @@ Kirimkan laporan bug, kontribusikan fitur baru dan ajukan pertanyaan di Otentikasi gagal untuk %s. Perbarui pengaturan server Anda. - Memeriksa pesan: %s:%s + Memeriksa pesan: %1$s:%2$s Memeriksa pesan Mengirimkan pesan: %s Mengirimkan pesan @@ -198,7 +198,7 @@ Kirimkan laporan bug, kontribusikan fitur baru dan ajukan pertanyaan di Buang teks kutipan Sunting teks kutipan Buang lampiran - Dari: %s <%s> + Dari: %1$s <%2$s> Kepada: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-is/strings.xml b/app/ui/legacy/src/main/res/values-is/strings.xml index 42c44c837d..28c7d9c8a0 100644 --- a/app/ui/legacy/src/main/res/values-is/strings.xml +++ b/app/ui/legacy/src/main/res/values-is/strings.xml @@ -149,7 +149,7 @@ Sendu inn villuskýrslur, leggðu fram nýja eiginleika og spurðu spurninga á %d ný skilaboð %d ný skilaboð - %d ólesin (%s) + %1$d ólesin (%2$s) + %1$d fleiri á %2$s Svara Merkja sem lesið @@ -165,7 +165,7 @@ Sendu inn villuskýrslur, leggðu fram nýja eiginleika og spurðu spurninga á Auðkenning mistókst fyrir %s. Uppfærðu stillingar póstþjónsins þíns. - Athuga með póst: %s:%s + Athuga með póst: %1$s:%2$s Athuga með póst Sendi póst: %s Sendi póst @@ -209,7 +209,7 @@ Sendu inn villuskýrslur, leggðu fram nýja eiginleika og spurðu spurninga á Fjarlægja tilvitnun Breyta tilvitnun Fjarlægja viðhengi - Frá: %s <%s> + Frá: %1$s <%2$s> Til: Afrit: Falið afrit: diff --git a/app/ui/legacy/src/main/res/values-it/strings.xml b/app/ui/legacy/src/main/res/values-it/strings.xml index f42bcb9fef..1ac4657b53 100644 --- a/app/ui/legacy/src/main/res/values-it/strings.xml +++ b/app/ui/legacy/src/main/res/values-it/strings.xml @@ -156,7 +156,7 @@ Invia segnalazioni di bug, contribuisci con nuove funzionalità e poni domande s %d nuovi messaggi %d nuovi messaggi - %d non letti (%s) + %1$d non letti (%2$s) + %1$d altri in %2$s Rispondi Letto @@ -175,7 +175,7 @@ Invia segnalazioni di bug, contribuisci con nuove funzionalità e poni domande s Errore su notifica È avvenuto un errore durante la creazione di un avviso di ricezione di un nuovo messaggio. Questo errore è probabilmente dovuto all\'avviso sonoro di ricezione messaggio, che può non essere indicato, oppure non è stato trovato. \n\nClicca qui per esaminare i parametri di notifica. - Controllo posta: %s:%s + Controllo posta: %1$s:%2$s Controllo posta Invio posta: %s Invio posta @@ -222,7 +222,7 @@ Invia segnalazioni di bug, contribuisci con nuove funzionalità e poni domande s Rimuovi testo citato Modifica testo citato Rimuovi allegato - Da: %s <%s> + Da: %1$s <%2$s> A: Cc: Ccn: diff --git a/app/ui/legacy/src/main/res/values-iw/strings.xml b/app/ui/legacy/src/main/res/values-iw/strings.xml index 52ce836d7a..4cb5040e4f 100644 --- a/app/ui/legacy/src/main/res/values-iw/strings.xml +++ b/app/ui/legacy/src/main/res/values-iw/strings.xml @@ -118,7 +118,7 @@ %d הודעות חדשות %d הודעות חדשות - %d לא נקראו (%s) + %1$d לא נקראו (%2$s) + %1$d עוד בקבוצה %2$s השב סמן כנקרא @@ -136,7 +136,7 @@ עדכן את הגדרות השרת. - בודק דוא"ל: %s:%s + בודק דואל: %1$s:%2$s בודק דוא\"ל שולח דוא"ל: %s שולח דוא\"ל @@ -179,7 +179,7 @@ מחק טקסט מצוטט ערוך טקסט מצוטט מחק קובץ מצורף - מ: %s <%s> + מ: %1$s <%2$s> ל: עותק: הצג תמונות diff --git a/app/ui/legacy/src/main/res/values-ja/strings.xml b/app/ui/legacy/src/main/res/values-ja/strings.xml index 5e974b73c4..1ba09ea6a5 100644 --- a/app/ui/legacy/src/main/res/values-ja/strings.xml +++ b/app/ui/legacy/src/main/res/values-ja/strings.xml @@ -151,7 +151,7 @@ K-9 は大多数のメールクライアントと同様に、ほとんどのフ %d 件の新着メッセージ - %d 未読 (%s) + %1$d 未読 (%2$s) + %1$d 続く %2$s 返信 既読 @@ -170,7 +170,7 @@ K-9 は大多数のメールクライアントと同様に、ほとんどのフ 通知エラー 新着メッセージのためのシステム通知の作成中にエラーが発生しました。通知サウンドが不足していることが原因と思われます。\n\nタップして通知設定を開いてください。 - メール確認中: %s:%s + メール確認中: %1$s:%2$s メール確認中 メール送信中: %s メール送信中 @@ -217,7 +217,7 @@ K-9 は大多数のメールクライアントと同様に、ほとんどのフ 引用文を削除します 引用文を編集します 添付ファイルの削除 - 送信者: %s <%s> + 送信者: %1$s <%2$s> 宛先: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-ko/strings.xml b/app/ui/legacy/src/main/res/values-ko/strings.xml index 7ecc6f9b3c..ae22e84f83 100644 --- a/app/ui/legacy/src/main/res/values-ko/strings.xml +++ b/app/ui/legacy/src/main/res/values-ko/strings.xml @@ -101,7 +101,7 @@ %d 새 메세지 - %d 통의 읽지 않은 메일 (%s) + %1$d 통의 읽지 않은 메일 (%2$s) + %1$d more on %2$s 답장 읽음 @@ -117,7 +117,7 @@ %s 계정 인증 오류. 서버 설정을 업데이트하세요. - 메일 체크 중: %s:%s + 메일 체크 중: %1$s:%2$s 메일 체크 중 메일 전송 중: %s 메일 전송 중 @@ -155,7 +155,7 @@ 인용 메세지 포함 인용된 메시지 제거 인용된 메시지 편집 - 발신자: %s <%s> + 발신자: %1$s <%2$s> 수신자: 참조: Bcc: diff --git a/app/ui/legacy/src/main/res/values-lt/strings.xml b/app/ui/legacy/src/main/res/values-lt/strings.xml index 6b046d1457..f2b3f14140 100644 --- a/app/ui/legacy/src/main/res/values-lt/strings.xml +++ b/app/ui/legacy/src/main/res/values-lt/strings.xml @@ -156,7 +156,7 @@ Pateikite pranešimus apie klaidas, prisidėkite prie naujų funkcijų kūrimo i %d nauji laiškai %d nauji laiškai - %d Neskaitytų (%s) + %1$d Neskaitytų (%2$s) + %1$d daugiau ant %2$s Atsakyti Pažymėti kaip skaitytą @@ -173,7 +173,7 @@ Pateikite pranešimus apie klaidas, prisidėkite prie naujų funkcijų kūrimo i Nepavyko nustatyti autentiškumo %s. Atnaujinkite serverio nustatymus. - Tikrinamas paštas: %s:%s + Tikrinamas paštas: %1$s:%2$s Tikrinamas paštas Siunčiamas paštas: %s Siunčiamas paštas @@ -221,7 +221,7 @@ Pateikite pranešimus apie klaidas, prisidėkite prie naujų funkcijų kūrimo i Pašalinti cituojamą tekstą Redaguoti cituojamą tekstą Pašalinti priedą - Nuo: %s <%s> + Nuo: %1$s <%2$s> Kam: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-lv/strings.xml b/app/ui/legacy/src/main/res/values-lv/strings.xml index 6185f0beda..abd2cefa32 100644 --- a/app/ui/legacy/src/main/res/values-lv/strings.xml +++ b/app/ui/legacy/src/main/res/values-lv/strings.xml @@ -151,7 +151,7 @@ pat %d vairāk %d jauna vēstule %d jaunas vēstules - %d Nelasītas (%s) + %1$d Nelasītas (%2$s) + %1$d vairāk %2$s Atbildēt Atzīmēt kā izlasītu @@ -167,7 +167,7 @@ pat %d vairāk Identifikācijas pārbaude kontam %s neizdevās. Atjaunojiet servera iestatījumus! - Pārbauda pastu: %s:%s + Pārbauda pastu: %1$s:%2$s Pārbauda pastu Sūta pastu: %s Sūta pastu @@ -214,7 +214,7 @@ pat %d vairāk Noņemt citēto tekstu Redigēt citēto tekstu Noņemt pielikumu - No: %s <%s> + No: %1$s <%2$s> Kam: Cc: Neredzamais: diff --git a/app/ui/legacy/src/main/res/values-ml/strings.xml b/app/ui/legacy/src/main/res/values-ml/strings.xml index 685299adf2..391203a794 100644 --- a/app/ui/legacy/src/main/res/values-ml/strings.xml +++ b/app/ui/legacy/src/main/res/values-ml/strings.xml @@ -149,7 +149,7 @@ പുതിയ %d സന്ദേശം പുതിയ %d സന്ദേശങ്ങൾ - %d വായിക്കാത്തത് (%s) + %1$d വായിക്കാത്തത് (%2$s) +%1$d %2$s ൽ കൂടുതൽ മറുപടി പറയുക വായന അടയാളപ്പെടുത്തുക @@ -166,7 +166,7 @@ %s നായുള്ള പ്രാമാണീകരണം പരാജയപ്പെട്ടു. നിങ്ങളുടെ സെർവർ ക്രമീകരണങ്ങൾ പുതുക്കുക. - മെയിൽ പരിശോധിക്കുന്നു: %s:%s + മെയിൽ പരിശോധിക്കുന്നു: %1$s:%2$s മെയിൽ പരിശോധിക്കുന്നു മെയിൽ അയയ്ക്കുന്നു: %s മെയിൽ അയയ്ക്കുന്നു @@ -213,7 +213,7 @@ ഉദ്ധരിച്ച വാചകം നീക്കംചെയ്യുക ഉദ്ധരിച്ച വാചകം എഡിറ്റുചെയ്യുക അറ്റാച്ചുമെന്റ് നീക്കംചെയ്യുക - പ്രേഷിതാവ്: %s <%s> + പ്രേഷിതാവ്: %1$s <%2$s> സ്വീകർത്താവ്: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-nb/strings.xml b/app/ui/legacy/src/main/res/values-nb/strings.xml index 5a0d0cfd39..f0c7cc0e46 100644 --- a/app/ui/legacy/src/main/res/values-nb/strings.xml +++ b/app/ui/legacy/src/main/res/values-nb/strings.xml @@ -141,7 +141,7 @@ til %d flere %d ny melding %d nye meldinger - %d Ulest(e) (%s) + %1$d Ulest(e) (%2$s) + %1$d flere på %2$s Svar Merk som lest @@ -157,7 +157,7 @@ til %d flere Autentisering feilet for %s. Oppdater serverinnstillingene dine. - Sjekker e-post: %s:%s + Sjekker e-post: %1$s:%2$s Sjekker e-post Sender e-post: %s Sender e-post @@ -199,7 +199,7 @@ til %d flere Fjern sitert tekst Rediger sitert tekst Fjern vedlegg - Fra: %s <%s> + Fra: %1$s <%2$s> Til: Kopi: Blindkopi: diff --git a/app/ui/legacy/src/main/res/values-nl/strings.xml b/app/ui/legacy/src/main/res/values-nl/strings.xml index 9861d682c3..50c3e15084 100644 --- a/app/ui/legacy/src/main/res/values-nl/strings.xml +++ b/app/ui/legacy/src/main/res/values-nl/strings.xml @@ -148,7 +148,7 @@ Graag foutrapporten sturen, bijdragen voor nieuwe functies en vragen stellen op %d nieuwe berichten %d nieuwe berichten - %d Ongelezen (%s) + %1$d Ongelezen (%2$s) + %1$d meer bij %2$s Antwoorden Gelezen @@ -167,7 +167,7 @@ Graag foutrapporten sturen, bijdragen voor nieuwe functies en vragen stellen op Meldingsfout Er is een fout opgetreden tijdens het maken van een systeemmelding voor een nieuw bericht. De reden is waarschijnlijk een ontbrekend meldingsgeluid.\n\nTik om meldingsinstellingen te openen. - Controleer berichten: %s:%s + Controleer berichten: %1$s:%2$s Controleren berichten Versturen van berichten: %s Berichten versturen @@ -214,7 +214,7 @@ Graag foutrapporten sturen, bijdragen voor nieuwe functies en vragen stellen op Citaattekst verwijderen Citaattekst bewerken Bijlage verwijderen - Van: %s <%s> + Van: %1$s <%2$s> Aan: CC: BCC: diff --git a/app/ui/legacy/src/main/res/values-pl/strings.xml b/app/ui/legacy/src/main/res/values-pl/strings.xml index 1ac2170efc..b19bc90c97 100644 --- a/app/ui/legacy/src/main/res/values-pl/strings.xml +++ b/app/ui/legacy/src/main/res/values-pl/strings.xml @@ -157,7 +157,7 @@ Wysłane z urządzenia Android za pomocą K-9 Mail. Proszę wybaczyć moją zwi %d nowych wiadomości %d nowych wiadomości - Nowe: %d (%s) + Nowe: %1$d (%2$s) + %1$d więcej na %2$s Odpowiedz Oznacz jako przeczytane @@ -176,7 +176,7 @@ Wysłane z urządzenia Android za pomocą K-9 Mail. Proszę wybaczyć moją zwi Błąd powiadomienia Wystąpił błąd podczas próby utworzenia powiadomienia systemowego dla nowej wiadomości. Powodem jest najprawdopodobniej brak dźwięku powiadomienia.\n\nStuknij, aby otworzyć ustawienia powiadomień. - Sprawdzam: %s:%s + Sprawdzam: %1$s:%2$s Sprawdzam Wysyłam: %s Wysyłam @@ -223,7 +223,7 @@ Wysłane z urządzenia Android za pomocą K-9 Mail. Proszę wybaczyć moją zwi Usuń cytowany tekst Edytuj cytowany tekst Usuń załącznik - Od: %s <%s> + Od: %1$s <%2$s> Do: DW: UDW: diff --git a/app/ui/legacy/src/main/res/values-pt-rBR/strings.xml b/app/ui/legacy/src/main/res/values-pt-rBR/strings.xml index 0437e04a04..faa31abe47 100644 --- a/app/ui/legacy/src/main/res/values-pt-rBR/strings.xml +++ b/app/ui/legacy/src/main/res/values-pt-rBR/strings.xml @@ -154,7 +154,7 @@ Por favor encaminhe relatórios de bugs, contribua com novos recursos e tire dú %d novas mensagens %d novas mensagens - %d não lidas (%s) + %1$d não lidas (%2$s) + %1$d a mais em %2$s Responder Marcar como lida @@ -173,7 +173,7 @@ Por favor encaminhe relatórios de bugs, contribua com novos recursos e tire dú Erro de notificação Ocorreu um erro ao tentar criar uma notificação de sistema para uma nova mensagem. O provável motivo é a ausência de um som de notificação.\n\nToque para abrir as configurações de notificação. - Verificando email: %s:%s + Verificando email: %1$s:%2$s Verificando email Enviando mensagem: %s Enviando mensagem @@ -220,7 +220,7 @@ Por favor encaminhe relatórios de bugs, contribua com novos recursos e tire dú Remover o texto citado Editar o texto citado Remover anexo - De: %s <%s> + De: %1$s <%2$s> Para: Cc: Cco: diff --git a/app/ui/legacy/src/main/res/values-pt-rPT/strings.xml b/app/ui/legacy/src/main/res/values-pt-rPT/strings.xml index 353730993a..2532006e67 100644 --- a/app/ui/legacy/src/main/res/values-pt-rPT/strings.xml +++ b/app/ui/legacy/src/main/res/values-pt-rPT/strings.xml @@ -135,7 +135,7 @@ Por favor envie relatórios de falhas, contribua com novas funcionalidades e col %d novas mensagens %d novas mensagens - %d não lidos (%s) + %1$d não lidos (%2$s) + %1$d em %2$s Responder Marcar como lido @@ -151,7 +151,7 @@ Por favor envie relatórios de falhas, contribua com novas funcionalidades e col A autenticação falhou para %s. Atualize as configurações do servidor. - A verificar correio: %s:%s + A verificar correio: %1$s:%2$s A verificar correio A enviar correio: %s A enviar correio @@ -193,7 +193,7 @@ Por favor envie relatórios de falhas, contribua com novas funcionalidades e col Remover texto citado Editar texto citado Remover anexo - De: %s <%s> + De: %1$s <%2$s> Para: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-ro/strings.xml b/app/ui/legacy/src/main/res/values-ro/strings.xml index 10d9b54b40..2112899ac3 100644 --- a/app/ui/legacy/src/main/res/values-ro/strings.xml +++ b/app/ui/legacy/src/main/res/values-ro/strings.xml @@ -152,7 +152,7 @@ cel mult încă %d %d mesaje noi %d mesaje noi - %d necitite (%s) + %1$d necitite (%2$s) + %1$d mai multe %2$s Răspunde Marchează ca citit @@ -169,7 +169,7 @@ cel mult încă %d Authentificare eșuată pentru %s. Actualizează setările serverului. - Se verifică emailul: %s:%s + Se verifică emailul: %1$s:%2$s Se verifică emailul Se trimite emailul: %s Se trimite emailul @@ -216,7 +216,7 @@ cel mult încă %d Elimină textul citat Editează textul citat Elimină atașamentul - De la: %s <%s> + De la: %1$s <%2$s> La: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-ru/strings.xml b/app/ui/legacy/src/main/res/values-ru/strings.xml index 63a77bd67e..ef85ff26e3 100644 --- a/app/ui/legacy/src/main/res/values-ru/strings.xml +++ b/app/ui/legacy/src/main/res/values-ru/strings.xml @@ -154,7 +154,7 @@ K-9 Mail — почтовый клиент для Android. %d новых сообщений %d новых сообщений - %d новых (%s) + %1$d новых (%2$s) + ещё %1$d в %2$s Ответить Прочитано @@ -171,7 +171,7 @@ K-9 Mail — почтовый клиент для Android. Сбой аутентификации для %s. Измените настройки сервера - Проверка %s:%s + Проверка %1$s:%2$s Проверка почты Отправка %s Отправка почты @@ -218,7 +218,7 @@ K-9 Mail — почтовый клиент для Android. Удалить цитату Правка цитаты Удалить вложение - От: %s <%s> + От: %1$s <%2$s> Кому: Копия: Скрытая: diff --git a/app/ui/legacy/src/main/res/values-sk/strings.xml b/app/ui/legacy/src/main/res/values-sk/strings.xml index 64f79ba155..da5ff48a84 100644 --- a/app/ui/legacy/src/main/res/values-sk/strings.xml +++ b/app/ui/legacy/src/main/res/values-sk/strings.xml @@ -146,7 +146,7 @@ Prosím, nahlasujte prípadné chyby, prispievajte novými funkciami a pýtajte %d nových správ %d nových správ - Počet neprečítaných správ: %d v %s + Počet neprečítaných správ: %1$d v %2$s + %1$d ďalších v %2$s Odpovedať Označiť ako prečítané @@ -162,7 +162,7 @@ Prosím, nahlasujte prípadné chyby, prispievajte novými funkciami a pýtajte Overenie pre %s zlyhalo. Aktualizujte nastavenia servera. - Kontrolovanie pošty: %s:%s + Kontrolovanie pošty: %1$s:%2$s Kontrolovanie pošty Odosielanie pošty: %s Odosielanie pošty @@ -204,7 +204,7 @@ Prosím, nahlasujte prípadné chyby, prispievajte novými funkciami a pýtajte Odstrániť citovaný text Upraviť citovaný text Odstrániť prílohu - Od: %s <%s> + Od: %1$s <%2$s> Komu: Kópia: Skrytá kópia (Bcc): diff --git a/app/ui/legacy/src/main/res/values-sl/strings.xml b/app/ui/legacy/src/main/res/values-sl/strings.xml index dd98849469..ca99d8e8b3 100644 --- a/app/ui/legacy/src/main/res/values-sl/strings.xml +++ b/app/ui/legacy/src/main/res/values-sl/strings.xml @@ -157,7 +157,7 @@ dodatnih %d sporočil %d nova sporočila %d novih sporočil - Neprebrano: %d (%s) + Neprebrano: %1$d (%2$s) in %1$d na %2$s Odgovori Označi kot prebrano @@ -176,7 +176,7 @@ dodatnih %d sporočil Napaka obveščanja Med poskusom ustvarjanja sistemskega obvestila za novo sporočilo je prišlo do napake. Razlog je najverjetneje manjkajoči zvok obvestila.\n\nTapni, za odpreti nastavitve obvestil. - Preverjanje pošte: %s:%s + Preverjanje pošte: %1$s:%2$s Preverjanje pošte Pošiljanje pošte: %s Pošiljanje pošte @@ -223,7 +223,7 @@ dodatnih %d sporočil Odstrani izvirno besedilo Uredi izvirno besedilo Odstrani prilogo - Od: %s <%s> + Od: %1$s <%2$s> Za: Kp: Skp: diff --git a/app/ui/legacy/src/main/res/values-sq/strings.xml b/app/ui/legacy/src/main/res/values-sq/strings.xml index 339aeb20ae..d7ac024845 100644 --- a/app/ui/legacy/src/main/res/values-sq/strings.xml +++ b/app/ui/legacy/src/main/res/values-sq/strings.xml @@ -154,7 +154,7 @@ Ju lutemi, parashtrim njoftimesh për të meta, kontribute për veçori të reaj %d mesazh i ri %d mesazhe të rinj - %d Të palexuar te (%s) + %1$d Të palexuar te (%2$s) + %1$d më tepër te %2$s Përgjigjuni Shënoje Si të Lexuar @@ -173,7 +173,7 @@ Ju lutemi, parashtrim njoftimesh për të meta, kontribute për veçori të reaj Gabim njoftimi Ndodhi një gabim teksa provohej të krijohej një njoftim sistemi për një mesazh të ri. Arsyeja ka shumë gjasa të jetë mungesa e një tingulli njoftimesh.\n\nPrekeni që të hapen rregullimet për njoftimet. - Po merret postë: %s:%s + Po merret postë: %1$s:%2$s Marrje poste Po dërgohet postë: %s Dërgim poste @@ -220,7 +220,7 @@ Ju lutemi, parashtrim njoftimesh për të meta, kontribute për veçori të reaj Hiqe tekstin e cituar Përpunoni tekstin e cituar Hiqe bashkëngjitjen - Nga: %s <%s> + Nga: %1$s <%2$s> Për: Cc: Bcc: diff --git a/app/ui/legacy/src/main/res/values-sr/strings.xml b/app/ui/legacy/src/main/res/values-sr/strings.xml index 50db72cb9f..1b488fcc6b 100644 --- a/app/ui/legacy/src/main/res/values-sr/strings.xml +++ b/app/ui/legacy/src/main/res/values-sr/strings.xml @@ -144,7 +144,7 @@ %d нове поруке %d нових порука - %d непрочитаних (%s) + %1$d непрочитаних (%2$s) + %1$d још на %2$s Одговори Означи прочитаним @@ -160,7 +160,7 @@ Аутентификација није успела за %s. Ажурирајте поставке сервера. - Проверавам пошту: %s:%s + Проверавам пошту: %1$s:%2$s Проверавам пошту Шаљем пошту: %s Шаљем пошту @@ -202,7 +202,7 @@ Уклони цитирани текст Уреди цитирани текст Уклони прилог - Од: %s <%s> + Од: %1$s <%2$s> За: Коп: СКоп: diff --git a/app/ui/legacy/src/main/res/values-sv/strings.xml b/app/ui/legacy/src/main/res/values-sv/strings.xml index 6d0655bc9c..8c84fcb8a3 100644 --- a/app/ui/legacy/src/main/res/values-sv/strings.xml +++ b/app/ui/legacy/src/main/res/values-sv/strings.xml @@ -153,7 +153,7 @@ Skicka in felrapporter, bidra med nya funktioner och ställ frågor på %d nytt meddelande %d nya meddelanden - %d olästa (%s) + %1$d olästa (%2$s) + %1$d fler på %2$s Svara Markera som läst @@ -172,7 +172,7 @@ Skicka in felrapporter, bidra med nya funktioner och ställ frågor på Aviseringsfel Ett fel uppstod vid försök att skapa en systemavisering för ett nytt meddelande. Anledningen är troligen att ett aviseringsljud saknas.\n\nTryck för att öppna aviseringsinställningarna. - Kontrollerar e-post: %s:%s + Kontrollerar e-post: %1$s:%2$s Kontrollerar e-post Skickar e-post: %s Skickar e-post @@ -219,7 +219,7 @@ Skicka in felrapporter, bidra med nya funktioner och ställ frågor på Ta bort citerad text Redigera citerad text Ta bort bilaga - Från: %s <%s> + Från: %1$s <%2$s> Till: Kopia: Blindkopia: diff --git a/app/ui/legacy/src/main/res/values-tr/strings.xml b/app/ui/legacy/src/main/res/values-tr/strings.xml index 875d6ead13..30fa3080d7 100644 --- a/app/ui/legacy/src/main/res/values-tr/strings.xml +++ b/app/ui/legacy/src/main/res/values-tr/strings.xml @@ -148,7 +148,7 @@ Microsoft Exchange ile konuşurken bazı tuhaflıklar yaşadığını not ediniz %d yeni mesaj %d yeni ileti - %d Okunmadı (%s) + %1$d Okunmadı (%2$s) %1$d daha fazla %2$s Yanıtla Okundu olarak işaretle @@ -165,7 +165,7 @@ Microsoft Exchange ile konuşurken bazı tuhaflıklar yaşadığını not ediniz %s için oturum açılamadı. Sunucu ayarlarınızı güncelleyin. - Posta kontrol ediliyor: %s:%s + Posta kontrol ediliyor: %1$s:%2$s Posta kontrol ediliyor Posta gönderiliyor: %s Posta gönderiliyor @@ -212,7 +212,7 @@ Microsoft Exchange ile konuşurken bazı tuhaflıklar yaşadığını not ediniz Alıntı yapılan metni sil Alıntı yapılan metni düzenle Eki kaldır - Kimden: %s <%s> + Kimden: %1$s <%2$s> Kime: Bilgi: Gizli Kopya: diff --git a/app/ui/legacy/src/main/res/values-uk/strings.xml b/app/ui/legacy/src/main/res/values-uk/strings.xml index 24e2f7ed10..87d41cbf78 100644 --- a/app/ui/legacy/src/main/res/values-uk/strings.xml +++ b/app/ui/legacy/src/main/res/values-uk/strings.xml @@ -155,7 +155,7 @@ K-9 Mail — це вільний клієнт електронної пошти %d нових повідомлень %d нових повідомлень - %d непрочитане(-их) (%s) + %1$d непрочитане(-их) (%2$s) ще %1$d у %2$s Відповісти Позначити прочитаним @@ -172,7 +172,7 @@ K-9 Mail — це вільний клієнт електронної пошти Помилка автентифікації для %s. Змініть налаштування сервера. - Перевірка пошти: %s:%s + Перевірка пошти: %1$s:%2$s Перевірка пошти Надсилання пошти: %s Надсилання пошти @@ -219,7 +219,7 @@ K-9 Mail — це вільний клієнт електронної пошти Видалити цитований текст Редагувати цитований текст Видалити вкладення - Від: %s <%s> + Від: %1$s <%2$s> Кому: Копія: Прихована копія: diff --git a/app/ui/legacy/src/main/res/values-zh-rCN/strings.xml b/app/ui/legacy/src/main/res/values-zh-rCN/strings.xml index 790c733ce3..51681ef9ba 100644 --- a/app/ui/legacy/src/main/res/values-zh-rCN/strings.xml +++ b/app/ui/legacy/src/main/res/values-zh-rCN/strings.xml @@ -151,7 +151,7 @@ K-9 Mail 是 Android 上一款功能强大的免费邮件客户端。 %d 条新消息 - 您有%d封未读邮件(%s + 您有%1$d封未读邮件(%2$s 加载 %2$s 上的更多 %1$d 条消息 回复 标记为已读 @@ -170,7 +170,7 @@ K-9 Mail 是 Android 上一款功能强大的免费邮件客户端。 通知错误 尝试创建新消息的系统通知时出错。最有可能的原因是缺少通知声音。\n\n轻按打开通知设置。 - 正在检查邮件:%s%s + 正在检查邮件:%1$s%2$s 正在检查邮件 正在发送邮件:%s 发送邮件 @@ -217,7 +217,7 @@ K-9 Mail 是 Android 上一款功能强大的免费邮件客户端。 删除引用文本 编辑引用文本 移除附件 - 发件人:%s <%s> + 发件人:%1$s <%2$s> 收件人: 抄送: 密送: diff --git a/app/ui/legacy/src/main/res/values-zh-rTW/strings.xml b/app/ui/legacy/src/main/res/values-zh-rTW/strings.xml index 55c22a4873..750f1bd384 100644 --- a/app/ui/legacy/src/main/res/values-zh-rTW/strings.xml +++ b/app/ui/legacy/src/main/res/values-zh-rTW/strings.xml @@ -151,7 +151,7 @@ K-9 Mail 是 Android 上一款功能強大,免費的電子郵件用戶端。 %d 條新訊息 - 您有%d封未讀郵件(%s + 您有%1$d封未讀郵件(%2$s + 來自%2$s已超過%1$d則訊息 回覆 標示為已讀取 @@ -168,7 +168,7 @@ K-9 Mail 是 Android 上一款功能強大,免費的電子郵件用戶端。 %s 登入失敗。請更新你的伺服器設定。 - 正在檢查郵件:%s:%s + 正在檢查郵件:%1$s:%2$s 正在檢查郵件 正在寄送郵件:%s 正在寄送郵件 @@ -215,7 +215,7 @@ K-9 Mail 是 Android 上一款功能強大,免費的電子郵件用戶端。 清除引用文字內容 編輯引用文字內容 移除附件 - 寄件人:%s <%s> + 寄件人:%1$s <%2$s> 收件人: 副本: 密件副本: -- GitLab From dcb3321870d4df20fbb2839a4974852e5464171a Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:21:48 +0200 Subject: [PATCH 07/85] Fix bug in `com.fsck.k9.logging.Timber` --- mail/common/src/main/java/com/fsck/k9/logging/Timber.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/common/src/main/java/com/fsck/k9/logging/Timber.kt b/mail/common/src/main/java/com/fsck/k9/logging/Timber.kt index e0912ec943..4e77f392cc 100644 --- a/mail/common/src/main/java/com/fsck/k9/logging/Timber.kt +++ b/mail/common/src/main/java/com/fsck/k9/logging/Timber.kt @@ -68,7 +68,7 @@ object Timber { @JvmStatic fun e(message: String?, vararg args: Any?) { - logger.e(message) + logger.e(message, *args) } @JvmStatic -- GitLab From a807373e99cf335f7a389b18f5eec8728697a5ad Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:28:43 +0200 Subject: [PATCH 08/85] Make sure overridden methods use same parameter names as super class --- .../src/main/java/com/fsck/k9/activity/MessageList.kt | 4 ++-- .../src/main/java/com/fsck/k9/backend/api/Backend.kt | 2 +- .../main/java/com/fsck/k9/backend/imap/ImapBackend.kt | 4 ++-- .../main/java/com/fsck/k9/backend/jmap/JmapBackend.kt | 4 ++-- .../main/java/com/fsck/k9/backend/pop3/Pop3Backend.kt | 4 ++-- .../k9mail/backend/testing/InMemoryBackendStorage.kt | 10 ++++++---- .../java/com/fsck/k9/backend/webdav/WebDavBackend.kt | 4 ++-- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt index cd530fbdd0..1534f3cb26 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt @@ -973,8 +973,8 @@ open class MessageList : progressBar!!.visibility = if (enable) View.VISIBLE else View.INVISIBLE } - override fun setMessageListProgress(progress: Int) { - progressBar!!.progress = progress + override fun setMessageListProgress(level: Int) { + progressBar!!.progress = level } override fun openMessage(messageReference: MessageReference) { diff --git a/backend/api/src/main/java/com/fsck/k9/backend/api/Backend.kt b/backend/api/src/main/java/com/fsck/k9/backend/api/Backend.kt index 2be2f601ed..5d657469a8 100644 --- a/backend/api/src/main/java/com/fsck/k9/backend/api/Backend.kt +++ b/backend/api/src/main/java/com/fsck/k9/backend/api/Backend.kt @@ -21,7 +21,7 @@ interface Backend { fun refreshFolderList() // TODO: Add a way to cancel the sync process - fun sync(folder: String, syncConfig: SyncConfig, listener: SyncListener) + fun sync(folderServerId: String, syncConfig: SyncConfig, listener: SyncListener) @Throws(MessagingException::class) fun downloadMessage(syncConfig: SyncConfig, folderServerId: String, messageServerId: String) diff --git a/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapBackend.kt b/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapBackend.kt index 510db124eb..a0bc860c91 100644 --- a/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapBackend.kt +++ b/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapBackend.kt @@ -51,8 +51,8 @@ class ImapBackend( commandRefreshFolderList.refreshFolderList() } - override fun sync(folder: String, syncConfig: SyncConfig, listener: SyncListener) { - imapSync.sync(folder, syncConfig, listener) + override fun sync(folderServerId: String, syncConfig: SyncConfig, listener: SyncListener) { + imapSync.sync(folderServerId, syncConfig, listener) } override fun downloadMessage(syncConfig: SyncConfig, folderServerId: String, messageServerId: String) { diff --git a/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapBackend.kt b/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapBackend.kt index 86987316ef..92b1ddaa75 100644 --- a/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapBackend.kt +++ b/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapBackend.kt @@ -45,8 +45,8 @@ class JmapBackend( commandRefreshFolderList.refreshFolderList() } - override fun sync(folder: String, syncConfig: SyncConfig, listener: SyncListener) { - commandSync.sync(folder, syncConfig, listener) + override fun sync(folderServerId: String, syncConfig: SyncConfig, listener: SyncListener) { + commandSync.sync(folderServerId, syncConfig, listener) } override fun downloadMessage(syncConfig: SyncConfig, folderServerId: String, messageServerId: String) { diff --git a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Backend.kt b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Backend.kt index fa1de215eb..64308ee0d9 100644 --- a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Backend.kt +++ b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Backend.kt @@ -38,8 +38,8 @@ class Pop3Backend( commandRefreshFolderList.refreshFolderList() } - override fun sync(folder: String, syncConfig: SyncConfig, listener: SyncListener) { - pop3Sync.sync(folder, syncConfig, listener) + override fun sync(folderServerId: String, syncConfig: SyncConfig, listener: SyncListener) { + pop3Sync.sync(folderServerId, syncConfig, listener) } override fun downloadMessage(syncConfig: SyncConfig, folderServerId: String, messageServerId: String) { diff --git a/backend/testing/src/main/java/app/k9mail/backend/testing/InMemoryBackendStorage.kt b/backend/testing/src/main/java/app/k9mail/backend/testing/InMemoryBackendStorage.kt index 53c0e20daa..909477c0c1 100644 --- a/backend/testing/src/main/java/app/k9mail/backend/testing/InMemoryBackendStorage.kt +++ b/backend/testing/src/main/java/app/k9mail/backend/testing/InMemoryBackendStorage.kt @@ -35,11 +35,13 @@ class InMemoryBackendStorage : BackendStorage { } private inner class InMemoryBackendFolderUpdater : BackendFolderUpdater { - override fun createFolders(foldersToCreate: List) { - foldersToCreate.forEach { folder -> - if (folders.containsKey(folder.serverId)) error("Folder ${folder.serverId} already present") + override fun createFolders(folders: List) { + folders.forEach { folder -> + if (this@InMemoryBackendStorage.folders.containsKey(folder.serverId)) { + error("Folder ${folder.serverId} already present") + } - folders[folder.serverId] = InMemoryBackendFolder(folder.name, folder.type) + this@InMemoryBackendStorage.folders[folder.serverId] = InMemoryBackendFolder(folder.name, folder.type) } } diff --git a/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavBackend.kt b/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavBackend.kt index 9f119ab8be..e0321d60a0 100644 --- a/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavBackend.kt +++ b/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavBackend.kt @@ -42,8 +42,8 @@ class WebDavBackend( commandGetFolders.refreshFolderList() } - override fun sync(folder: String, syncConfig: SyncConfig, listener: SyncListener) { - webDavSync.sync(folder, syncConfig, listener) + override fun sync(folderServerId: String, syncConfig: SyncConfig, listener: SyncListener) { + webDavSync.sync(folderServerId, syncConfig, listener) } override fun downloadMessage(syncConfig: SyncConfig, folderServerId: String, messageServerId: String) { -- GitLab From e083b6f31d70c715f4b6b807794a4087cfbd52fb Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:31:28 +0200 Subject: [PATCH 09/85] Suppress warning about inlining --- .../src/main/java/com/fsck/k9/backend/jmap/JmapExtensions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapExtensions.kt b/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapExtensions.kt index 12a6cfc47a..0fcc19604d 100644 --- a/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapExtensions.kt +++ b/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapExtensions.kt @@ -18,6 +18,7 @@ internal inline fun JmapRequest.Call.getMainRespons return methodResponses.getMainResponseBlocking() } +@Suppress("NOTHING_TO_INLINE") internal inline fun ListenableFuture.futureGetOrThrow(): T { return try { get() -- GitLab From 997d993413d33d19205c67ec121da2cb9df7c9d3 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:33:54 +0200 Subject: [PATCH 10/85] Remove unnecessary safe calls --- app/core/src/main/java/com/fsck/k9/LocalKeyStoreManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/LocalKeyStoreManager.kt b/app/core/src/main/java/com/fsck/k9/LocalKeyStoreManager.kt index c073646689..4933879bda 100644 --- a/app/core/src/main/java/com/fsck/k9/LocalKeyStoreManager.kt +++ b/app/core/src/main/java/com/fsck/k9/LocalKeyStoreManager.kt @@ -48,11 +48,11 @@ class LocalKeyStoreManager( * certificates for the incoming and outgoing servers. */ fun deleteCertificates(account: Account) { - account.incomingServerSettings?.let { serverSettings -> + account.incomingServerSettings.let { serverSettings -> localKeyStore.deleteCertificate(serverSettings.host!!, serverSettings.port) } - account.outgoingServerSettings?.let { serverSettings -> + account.outgoingServerSettings.let { serverSettings -> localKeyStore.deleteCertificate(serverSettings.host!!, serverSettings.port) } } -- GitLab From 29a530a6fbb2d1f1ef581bade1f9fc3f8d1543db Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:36:20 +0200 Subject: [PATCH 11/85] Remove unused method --- .../com/fsck/k9/storage/messages/FlagMessageOperations.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/FlagMessageOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/FlagMessageOperations.kt index 589d07aaf9..3abb91d177 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/FlagMessageOperations.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/FlagMessageOperations.kt @@ -15,7 +15,7 @@ internal class FlagMessageOperations(private val lockableDatabase: LockableDatab if (flag in SPECIAL_FLAGS) { setSpecialFlags(messageIds, flag, set) } else { - rebuildFlagsColumnValue(messageIds, flag, set) + throw UnsupportedOperationException("not implemented") } } @@ -54,10 +54,6 @@ internal class FlagMessageOperations(private val lockableDatabase: LockableDatab } } - private fun rebuildFlagsColumnValue(messageIds: Collection, flag: Flag, set: Boolean) { - throw UnsupportedOperationException("not implemented") - } - private fun rebuildFlagsColumnValue(folderId: Long, messageServerId: String, flag: Flag, set: Boolean) { lockableDatabase.execute(true) { database -> val oldFlags = database.readFlagsColumn(folderId, messageServerId) -- GitLab From 28f7318649294c6a00ffdad49f04653b466b4d92 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:37:36 +0200 Subject: [PATCH 12/85] Remove unused parameter --- .../src/main/java/com/fsck/k9/activity/MessageCompose.java | 2 +- .../java/com/fsck/k9/activity/compose/RecipientPresenter.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java index fd616150c3..81b55765ad 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -936,7 +936,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, updateSignature(); updateMessageFormat(); replyToPresenter.setIdentity(identity); - recipientPresenter.onSwitchIdentity(identity); + recipientPresenter.onSwitchIdentity(); } private void updateFrom() { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt index 11eec7594a..7d2e6d0672 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt @@ -12,7 +12,6 @@ import android.view.Menu import androidx.core.content.ContextCompat import androidx.loader.app.LoaderManager import com.fsck.k9.Account -import com.fsck.k9.Identity import com.fsck.k9.K9 import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState import com.fsck.k9.activity.compose.ComposeCryptoStatus.SendErrorState @@ -323,7 +322,7 @@ class RecipientPresenter( openPgpApiManager.setOpenPgpProvider(openPgpProvider, openPgpCallback) } - fun onSwitchIdentity(identity: Identity) { + fun onSwitchIdentity() { // TODO decide what actually to do on identity switch? asyncUpdateCryptoStatus() } -- GitLab From 6aec3a76c86e147970ce11f6ecf28a9a2f8db7ee Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:41:48 +0200 Subject: [PATCH 13/85] Add nullability annotation to avoid warning in Kotlin code --- .../main/java/org/openintents/openpgp/OpenPgpApiManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpApiManager.java b/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpApiManager.java index aa82dfdcf2..57516d0b6d 100644 --- a/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpApiManager.java +++ b/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/OpenPgpApiManager.java @@ -2,6 +2,8 @@ package org.openintents.openpgp; import android.app.PendingIntent; + +import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle.Event; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; @@ -224,6 +226,7 @@ public class OpenPgpApiManager implements LifecycleObserver { return openPgpProviderName != null ? openPgpProviderName : openPgpProvider; } + @NonNull public OpenPgpProviderState getOpenPgpProviderState() { return openPgpProviderState; } -- GitLab From 9f0d2ada3caca70c9aab3b77875f7cdc32fd0a2f Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:42:30 +0200 Subject: [PATCH 14/85] Remove unnecessary `else` cases in `when` expressions --- .../java/com/fsck/k9/activity/compose/RecipientPresenter.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt index 7d2e6d0672..f847caeac7 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt @@ -622,14 +622,12 @@ class RecipientPresenter( SendErrorState.ENABLED_ERROR -> recipientMvpView.showOpenPgpEnabledErrorDialog(false) SendErrorState.PROVIDER_ERROR -> recipientMvpView.showErrorOpenPgpConnection() SendErrorState.KEY_CONFIG_ERROR -> recipientMvpView.showErrorNoKeyConfigured() - else -> throw AssertionError("not all error states handled, this is a bug!") } } fun showPgpAttachError(attachErrorState: AttachErrorState) { when (attachErrorState) { AttachErrorState.IS_INLINE -> recipientMvpView.showErrorInlineAttach() - else -> throw AssertionError("not all error states handled, this is a bug!") } } -- GitLab From eb12ad538d0e212e430e49d141dcc701a6bc39b1 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:45:03 +0200 Subject: [PATCH 15/85] Remove unused functionality --- .../fsck/k9/ui/messageview/AttachmentController.java | 12 ------------ .../fsck/k9/ui/messageview/MessageViewFragment.kt | 4 ---- 2 files changed, 16 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java index 7eaf416782..acd68a96c9 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java @@ -220,11 +220,6 @@ public class AttachmentController { private class ViewAttachmentAsyncTask extends AsyncTask { - @Override - protected void onPreExecute() { - messageViewFragment.disableAttachmentButtons(attachment); - } - @Override protected Intent doInBackground(Void... params) { return getBestViewIntent(); @@ -233,7 +228,6 @@ public class AttachmentController { @Override protected void onPostExecute(Intent intent) { viewAttachment(intent); - messageViewFragment.enableAttachmentButtons(attachment); } private void viewAttachment(Intent intent) { @@ -250,11 +244,6 @@ public class AttachmentController { private class SaveAttachmentAsyncTask extends AsyncTask { - @Override - protected void onPreExecute() { - messageViewFragment.disableAttachmentButtons(attachment); - } - @Override protected Boolean doInBackground(Uri... params) { try { @@ -269,7 +258,6 @@ public class AttachmentController { @Override protected void onPostExecute(Boolean success) { - messageViewFragment.enableAttachmentButtons(attachment); if (!success) { displayAttachmentNotSavedMessage(); } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index 9eec05e6f5..6cf2845565 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -736,10 +736,6 @@ class MessageViewFragment : startActivity(intent) } - fun disableAttachmentButtons(attachment: AttachmentViewInfo?) = Unit - - fun enableAttachmentButtons(attachment: AttachmentViewInfo?) = Unit - fun runOnMainThread(runnable: Runnable) { requireActivity().runOnUiThread(runnable) } -- GitLab From 713082fe5590aff094fb6cb8a464d9dd97ea985c Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 17:50:46 +0200 Subject: [PATCH 16/85] Remove unnecessary elvis operator --- .../java/com/fsck/k9/widget/unread/UnreadWidgetDataProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetDataProvider.kt b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetDataProvider.kt index 5690639c17..4766a43beb 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetDataProvider.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/widget/unread/UnreadWidgetDataProvider.kt @@ -35,7 +35,7 @@ class UnreadWidgetDataProvider( private fun loadSearchAccountData(configuration: UnreadWidgetConfiguration): UnreadWidgetData { val searchAccount = getSearchAccount(configuration.accountUuid) - val title = searchAccount.name ?: searchAccount.email + val title = searchAccount.name val unreadCount = messagingController.getUnreadMessageCount(searchAccount) val clickIntent = MessageList.intentDisplaySearch(context, searchAccount.relatedSearch, false, true, true) -- GitLab From 46ca47facd172accb0f086427661264d1c4c7d28 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 18:18:31 +0200 Subject: [PATCH 17/85] Remove unused parameter --- app/core/src/main/java/com/fsck/k9/Core.kt | 2 +- app/core/src/main/java/com/fsck/k9/Preferences.kt | 5 ++--- .../src/main/java/com/fsck/k9/mailstore/LocalStore.java | 2 +- .../java/com/fsck/k9/preferences/SettingsImporter.java | 4 ++-- .../java/com/fsck/k9/provider/AttachmentProvider.java | 6 +++--- .../src/main/java/com/fsck/k9/provider/EmailProvider.java | 3 +-- .../java/com/fsck/k9/provider/RawMessageProvider.java | 2 +- .../java/com/fsck/k9/service/DatabaseUpgradeService.java | 2 +- .../com/fsck/k9/controller/MessagingControllerTest.java | 2 +- .../com/fsck/k9/preferences/SettingsImporterTest.java | 2 +- .../main/java/com/fsck/k9/external/MessageProvider.java | 6 +++--- .../src/main/java/com/fsck/k9/activity/AccountList.java | 2 +- .../main/java/com/fsck/k9/activity/ChooseIdentity.java | 2 +- .../src/main/java/com/fsck/k9/activity/EditIdentity.kt | 4 ++-- .../main/java/com/fsck/k9/activity/ManageIdentities.java | 2 +- .../java/com/fsck/k9/activity/MessageLoaderHelper.java | 4 ++-- .../main/java/com/fsck/k9/activity/UpgradeDatabases.java | 2 +- .../com/fsck/k9/activity/compose/IdentityAdapter.java | 2 +- .../java/com/fsck/k9/activity/compose/MessageActions.java | 2 +- .../fsck/k9/activity/setup/AccountSetupComposition.java | 6 +++--- .../com/fsck/k9/activity/setup/AccountSetupIncoming.java | 6 +++--- .../com/fsck/k9/activity/setup/AccountSetupNames.java | 4 ++-- .../com/fsck/k9/activity/setup/AccountSetupOptions.java | 4 ++-- .../com/fsck/k9/activity/setup/AccountSetupOutgoing.java | 8 ++++---- .../com/fsck/k9/ui/messageview/AttachmentController.java | 2 +- .../k9/ui/settings/account/OpenPgpAppSelectDialog.java | 4 ++-- 26 files changed, 44 insertions(+), 46 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/Core.kt b/app/core/src/main/java/com/fsck/k9/Core.kt index 7282a86526..75e8201ec3 100644 --- a/app/core/src/main/java/com/fsck/k9/Core.kt +++ b/app/core/src/main/java/com/fsck/k9/Core.kt @@ -45,7 +45,7 @@ object Core : EarlyInit { @JvmStatic fun setServicesEnabled(context: Context) { val appContext = context.applicationContext - val acctLength = Preferences.getPreferences(appContext).accounts.size + val acctLength = Preferences.getPreferences().accounts.size val enable = acctLength > 0 setServicesEnabled(appContext, enable) diff --git a/app/core/src/main/java/com/fsck/k9/Preferences.kt b/app/core/src/main/java/com/fsck/k9/Preferences.kt index 4120025c89..7e66d18f85 100644 --- a/app/core/src/main/java/com/fsck/k9/Preferences.kt +++ b/app/core/src/main/java/com/fsck/k9/Preferences.kt @@ -1,6 +1,5 @@ package com.fsck.k9 -import android.content.Context import androidx.annotation.GuardedBy import androidx.annotation.RestrictTo import com.fsck.k9.mail.MessagingException @@ -291,8 +290,8 @@ class Preferences internal constructor( companion object { @JvmStatic - fun getPreferences(context: Context): Preferences { - return DI.get(Preferences::class.java) + fun getPreferences(): Preferences { + return DI.get() } } } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java index 1fb427d6cd..4e33ebfea2 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -227,7 +227,7 @@ public class LocalStore { } protected Preferences getPreferences() { - return Preferences.getPreferences(context); + return Preferences.getPreferences(); } public OutboxStateRepository getOutboxStateRepository() { diff --git a/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java b/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java index 5e18598c37..e00d161dde 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java @@ -188,7 +188,7 @@ public class SettingsImporter { Imported imported = parseSettings(inputStream, globalSettings, accountUuids, false); - Preferences preferences = Preferences.getPreferences(context); + Preferences preferences = Preferences.getPreferences(); Storage storage = preferences.getStorage(); if (globalSettings) { @@ -331,7 +331,7 @@ public class SettingsImporter { AccountDescription original = new AccountDescription(account.name, account.uuid); - Preferences prefs = Preferences.getPreferences(context); + Preferences prefs = Preferences.getPreferences(); List accounts = prefs.getAccounts(); String uuid = account.uuid; diff --git a/app/core/src/main/java/com/fsck/k9/provider/AttachmentProvider.java b/app/core/src/main/java/com/fsck/k9/provider/AttachmentProvider.java index 21a91725b7..36e0644245 100644 --- a/app/core/src/main/java/com/fsck/k9/provider/AttachmentProvider.java +++ b/app/core/src/main/java/com/fsck/k9/provider/AttachmentProvider.java @@ -96,7 +96,7 @@ public class AttachmentProvider extends ContentProvider { final AttachmentInfo attachmentInfo; try { - final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); + final Account account = Preferences.getPreferences().getAccount(accountUuid); attachmentInfo = DI.get(LocalStoreProvider.class).getInstance(account).getAttachmentInfo(id); } catch (MessagingException e) { Timber.e(e, "Unable to retrieve attachment info from local store for ID: %s", id); @@ -143,7 +143,7 @@ public class AttachmentProvider extends ContentProvider { private String getType(String accountUuid, String id, String mimeType) { String type; - final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); + final Account account = Preferences.getPreferences().getAccount(accountUuid); try { final LocalStore localStore = DI.get(LocalStoreProvider.class).getInstance(account); @@ -182,7 +182,7 @@ public class AttachmentProvider extends ContentProvider { @Nullable private OpenPgpDataSource getAttachmentDataSource(String accountUuid, String attachmentId) throws MessagingException { - final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); + final Account account = Preferences.getPreferences().getAccount(accountUuid); LocalStore localStore = DI.get(LocalStoreProvider.class).getInstance(account); return localStore.getAttachmentDataSource(attachmentId); } diff --git a/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java b/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java index f24173dbe5..da70da7827 100644 --- a/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java +++ b/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java @@ -505,8 +505,7 @@ public class EmailProvider extends ContentProvider { private Account getAccount(String accountUuid) { if (mPreferences == null) { - Context appContext = getContext().getApplicationContext(); - mPreferences = Preferences.getPreferences(appContext); + mPreferences = Preferences.getPreferences(); } Account account = mPreferences.getAccount(accountUuid); diff --git a/app/core/src/main/java/com/fsck/k9/provider/RawMessageProvider.java b/app/core/src/main/java/com/fsck/k9/provider/RawMessageProvider.java index 37fcfddeb1..754bc125ca 100644 --- a/app/core/src/main/java/com/fsck/k9/provider/RawMessageProvider.java +++ b/app/core/src/main/java/com/fsck/k9/provider/RawMessageProvider.java @@ -174,7 +174,7 @@ public class RawMessageProvider extends ContentProvider { long folderId = messageReference.getFolderId(); String uid = messageReference.getUid(); - Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); + Account account = Preferences.getPreferences().getAccount(accountUuid); if (account == null) { Timber.w("Account not found: %s", accountUuid); return null; diff --git a/app/core/src/main/java/com/fsck/k9/service/DatabaseUpgradeService.java b/app/core/src/main/java/com/fsck/k9/service/DatabaseUpgradeService.java index efc8bf6208..6befea43a8 100644 --- a/app/core/src/main/java/com/fsck/k9/service/DatabaseUpgradeService.java +++ b/app/core/src/main/java/com/fsck/k9/service/DatabaseUpgradeService.java @@ -179,7 +179,7 @@ public class DatabaseUpgradeService extends Service { * Upgrade the accounts' databases. */ private void upgradeDatabases() { - Preferences preferences = Preferences.getPreferences(this); + Preferences preferences = Preferences.getPreferences(); List accounts = preferences.getAccounts(); mProgressEnd = accounts.size(); diff --git a/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java b/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java index 978788b78a..b1d6821f39 100644 --- a/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java +++ b/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java @@ -139,7 +139,7 @@ public class MessagingControllerTest extends K9RobolectricTest { MockitoAnnotations.initMocks(this); appContext = RuntimeEnvironment.application; - preferences = Preferences.getPreferences(appContext); + preferences = Preferences.getPreferences(); controller = new MessagingController(appContext, notificationController, notificationStrategy, localStoreProvider, messageCountsProvider, backendManager, preferences, messageStoreManager, diff --git a/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java b/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java index fcfdd48fd7..6b9be20b83 100644 --- a/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java +++ b/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java @@ -27,7 +27,7 @@ public class SettingsImporterTest extends K9RobolectricTest { } private void deletePreExistingAccounts() { - Preferences preferences = Preferences.getPreferences(RuntimeEnvironment.application); + Preferences preferences = Preferences.getPreferences(); preferences.clearAccounts(); } diff --git a/app/k9mail/src/main/java/com/fsck/k9/external/MessageProvider.java b/app/k9mail/src/main/java/com/fsck/k9/external/MessageProvider.java index 8ef22f8e80..759744d9d1 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/external/MessageProvider.java +++ b/app/k9mail/src/main/java/com/fsck/k9/external/MessageProvider.java @@ -149,7 +149,7 @@ public class MessageProvider extends ContentProvider { // get account Account myAccount = null; - for (Account account : Preferences.getPreferences(getContext()).getAccounts()) { + for (Account account : Preferences.getPreferences().getAccounts()) { if (account.getAccountNumber() == accountId) { myAccount = account; } @@ -601,7 +601,7 @@ public class MessageProvider extends ContentProvider { MatrixCursor cursor = new MatrixCursor(projection); - for (Account account : Preferences.getPreferences(getContext()).getAccounts()) { + for (Account account : Preferences.getPreferences().getAccounts()) { Object[] values = new Object[projection.length]; int fieldIndex = 0; @@ -667,7 +667,7 @@ public class MessageProvider extends ContentProvider { Context context = getContext(); MessagingController controller = MessagingController.getInstance(context); - Collection accounts = Preferences.getPreferences(context).getAccounts(); + Collection accounts = Preferences.getPreferences().getAccounts(); for (Account account : accounts) { if (account.getAccountNumber() == accountNumber) { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/AccountList.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/AccountList.java index 6e42b8621c..a80d743bbf 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/AccountList.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/AccountList.java @@ -155,7 +155,7 @@ public abstract class AccountList extends K9ListActivity implements OnItemClickL class LoadAccounts extends AsyncTask> { @Override protected List doInBackground(Void... params) { - return Preferences.getPreferences(getApplicationContext()).getAccounts(); + return Preferences.getPreferences().getAccounts(); } @Override diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/ChooseIdentity.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/ChooseIdentity.java index df88e54f9a..898d6d6483 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/ChooseIdentity.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/ChooseIdentity.java @@ -37,7 +37,7 @@ public class ChooseIdentity extends K9ListActivity { getListView().setChoiceMode(ListView.CHOICE_MODE_NONE); Intent intent = getIntent(); String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + mAccount = Preferences.getPreferences().getAccount(accountUuid); adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1); diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/EditIdentity.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/EditIdentity.kt index 1992c1cb27..149f9ebbfe 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/EditIdentity.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/EditIdentity.kt @@ -35,7 +35,7 @@ class EditIdentity : K9Activity() { identityIndex = intent.getIntExtra(EXTRA_IDENTITY_INDEX, -1) val accountUuid = intent.getStringExtra(EXTRA_ACCOUNT) ?: error("Missing account UUID") - account = Preferences.getPreferences(this).getAccount(accountUuid) ?: error("Couldn't find account") + account = Preferences.getPreferences().getAccount(accountUuid) ?: error("Couldn't find account") identity = when { savedInstanceState != null -> savedInstanceState.getParcelable(EXTRA_IDENTITY) ?: error("Missing state") @@ -91,7 +91,7 @@ class EditIdentity : K9Activity() { identities.add(identityIndex, identity) } - Preferences.getPreferences(applicationContext).saveAccount(account) + Preferences.getPreferences().saveAccount(account) finish() } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/ManageIdentities.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/ManageIdentities.java index 5e5b898041..501188a871 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/ManageIdentities.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/ManageIdentities.java @@ -139,7 +139,7 @@ public class ManageIdentities extends ChooseIdentity { private void saveIdentities() { if (mIdentitiesChanged) { mAccount.setIdentities(identities); - Preferences.getPreferences(getApplicationContext()).saveAccount(mAccount); + Preferences.getPreferences().saveAccount(mAccount); } finish(); } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageLoaderHelper.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageLoaderHelper.java index 229b481034..a591aeed5c 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageLoaderHelper.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageLoaderHelper.java @@ -115,7 +115,7 @@ public class MessageLoaderHelper { public void asyncStartOrResumeLoadingMessage(MessageReference messageReference, Parcelable cachedDecryptionResult) { onlyLoadMetadata = false; this.messageReference = messageReference; - this.account = Preferences.getPreferences(context).getAccount(messageReference.getAccountUuid()); + this.account = Preferences.getPreferences().getAccount(messageReference.getAccountUuid()); if (cachedDecryptionResult != null) { if (cachedDecryptionResult instanceof OpenPgpDecryptionResult) { @@ -132,7 +132,7 @@ public class MessageLoaderHelper { public void asyncStartOrResumeLoadingMessageMetadata(MessageReference messageReference) { onlyLoadMetadata = true; this.messageReference = messageReference; - this.account = Preferences.getPreferences(context).getAccount(messageReference.getAccountUuid()); + this.account = Preferences.getPreferences().getAccount(messageReference.getAccountUuid()); startOrResumeLocalMessageLoader(); } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/UpgradeDatabases.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/UpgradeDatabases.java index 52e90bc51d..92f9c30af8 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/UpgradeDatabases.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/UpgradeDatabases.java @@ -115,7 +115,7 @@ public class UpgradeDatabases extends K9Activity { return; } - mPreferences = Preferences.getPreferences(getApplicationContext()); + mPreferences = Preferences.getPreferences(); initializeLayout(); diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/IdentityAdapter.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/IdentityAdapter.java index 00bc9469ac..f6cf46f7e7 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/IdentityAdapter.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/IdentityAdapter.java @@ -32,7 +32,7 @@ public class IdentityAdapter extends BaseAdapter { Context.LAYOUT_INFLATER_SERVICE); List items = new ArrayList<>(); - Preferences prefs = Preferences.getPreferences(context.getApplicationContext()); + Preferences prefs = Preferences.getPreferences(); Collection accounts = prefs.getAccounts(); for (Account account : accounts) { items.add(account); diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageActions.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageActions.java index 6c929d4858..e1cd9899ac 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageActions.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageActions.java @@ -17,7 +17,7 @@ public class MessageActions { * activity. */ public static void actionCompose(Context context, Account account) { - Account defaultAccount = Preferences.getPreferences(context).getDefaultAccount(); + Account defaultAccount = Preferences.getPreferences().getDefaultAccount(); if (account == null && defaultAccount == null) { AccountSetupBasics.actionNewAccount(context); } else { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java index 10315606fd..b463ea7c69 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java @@ -46,7 +46,7 @@ public class AccountSetupComposition extends K9Activity { super.onCreate(savedInstanceState); String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + mAccount = Preferences.getPreferences().getAccount(accountUuid); setLayout(R.layout.account_setup_composition); setTitle(R.string.account_settings_composition_title); @@ -61,7 +61,7 @@ public class AccountSetupComposition extends K9Activity { */ if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) { accountUuid = savedInstanceState.getString(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + mAccount = Preferences.getPreferences().getAccount(accountUuid); } mAccountName = findViewById(R.id.account_name); @@ -129,7 +129,7 @@ public class AccountSetupComposition extends K9Activity { mAccount.setSignatureBeforeQuotedText(isSignatureBeforeQuotedText); } - Preferences.getPreferences(getApplicationContext()).saveAccount(mAccount); + Preferences.getPreferences().saveAccount(mAccount); } @Override diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java index 61a6cdeb8c..be25116dfa 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -158,7 +158,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789")); String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + mAccount = Preferences.getPreferences().getAccount(accountUuid); mMakeDefault = getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false); /* @@ -167,7 +167,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener */ if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) { accountUuid = savedInstanceState.getString(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + mAccount = Preferences.getPreferences().getAccount(accountUuid); } boolean oAuthSupported = mAccount.getIncomingServerSettings().type.equals(Protocols.IMAP); @@ -539,7 +539,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener if (resultCode == RESULT_OK) { if (Intent.ACTION_EDIT.equals(getIntent().getAction())) { - Preferences.getPreferences(getApplicationContext()).saveAccount(mAccount); + Preferences.getPreferences().saveAccount(mAccount); finish(); } else { /* diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupNames.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupNames.java index f9f222a70f..603232baee 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupNames.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupNames.java @@ -65,7 +65,7 @@ public class AccountSetupNames extends K9Activity implements OnClickListener { mName.setKeyListener(TextKeyListener.getInstance(false, Capitalize.WORDS)); String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + mAccount = Preferences.getPreferences().getAccount(accountUuid); String senderName = mAccount.getSenderName(); if (senderName != null) { @@ -88,7 +88,7 @@ public class AccountSetupNames extends K9Activity implements OnClickListener { } mAccount.setSenderName(mName.getText().toString()); mAccount.markSetupFinished(); - Preferences.getPreferences(getApplicationContext()).saveAccount(mAccount); + Preferences.getPreferences().saveAccount(mAccount); finishAffinity(); MessageList.launch(this, mAccount); } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOptions.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOptions.java index 515762aa7e..a3374c3e27 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOptions.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOptions.java @@ -92,7 +92,7 @@ public class AccountSetupOptions extends K9Activity implements OnClickListener { mDisplayCountView.setAdapter(displayCountsAdapter); String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + mAccount = Preferences.getPreferences().getAccount(accountUuid); mNotifyView.setChecked(mAccount.isNotifyNewMail()); SpinnerOption.setSpinnerOptionValue(mCheckFrequencyView, mAccount @@ -111,7 +111,7 @@ public class AccountSetupOptions extends K9Activity implements OnClickListener { mAccount.setFolderPushMode(Account.FolderMode.NONE); - Preferences.getPreferences(getApplicationContext()).saveAccount(mAccount); + Preferences.getPreferences().saveAccount(mAccount); Core.setServicesEnabled(this); AccountSetupNames.actionSetNames(this, mAccount); } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java index 90529eb430..91a8f7cdcb 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java @@ -101,7 +101,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, setTitle(R.string.account_setup_outgoing_title); String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + mAccount = Preferences.getPreferences().getAccount(accountUuid); ServerSettings incomingServerSettings = mAccount.getIncomingServerSettings(); if (incomingServerSettings.type.equals(Protocols.WEBDAV)) { @@ -138,7 +138,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, //FIXME: get Account object again? accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + mAccount = Preferences.getPreferences().getAccount(accountUuid); /* * If we're being reloaded we override the original account with the one @@ -146,7 +146,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, */ if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) { accountUuid = savedInstanceState.getString(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + mAccount = Preferences.getPreferences().getAccount(accountUuid); } boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction()); @@ -495,7 +495,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, if (resultCode == RESULT_OK) { if (Intent.ACTION_EDIT.equals(getIntent().getAction())) { - Preferences.getPreferences(getApplicationContext()).saveAccount(mAccount); + Preferences.getPreferences().saveAccount(mAccount); finish(); } else { AccountSetupOptions.actionOptions(this, mAccount); diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java index acd68a96c9..83d1431adb 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java @@ -86,7 +86,7 @@ public class AttachmentController { private void downloadAttachment(LocalPart localPart, final Runnable attachmentDownloadedCallback) { String accountUuid = localPart.getAccountUuid(); - Account account = Preferences.getPreferences(context).getAccount(accountUuid); + Account account = Preferences.getPreferences().getAccount(accountUuid); LocalMessage message = localPart.getMessage(); messageViewFragment.showAttachmentLoadingDialog(); diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/OpenPgpAppSelectDialog.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/OpenPgpAppSelectDialog.java index d9045bc4fa..18af4fcf6d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/OpenPgpAppSelectDialog.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/OpenPgpAppSelectDialog.java @@ -66,7 +66,7 @@ public class OpenPgpAppSelectDialog extends K9Activity { super.onCreate(savedInstanceState); String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); - account = Preferences.getPreferences(this).getAccount(accountUuid); + account = Preferences.getPreferences().getAccount(accountUuid); } @Override @@ -283,7 +283,7 @@ public class OpenPgpAppSelectDialog extends K9Activity { private void persistOpenPgpProviderSetting(String selectedPackage) { account.setOpenPgpProvider(selectedPackage); - Preferences.getPreferences(getApplicationContext()).saveAccount(account); + Preferences.getPreferences().saveAccount(account); } private static class OpenPgpProviderEntry { -- GitLab From 03189fae466b642d9fff2a7fb1052aca0b8e1b26 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 18:33:18 +0200 Subject: [PATCH 18/85] Replace usages of the deprecated `RuntimeEnvironment.application` --- .../autocrypt/AutocryptHeaderParserTest.java | 2 +- .../fsck/k9/cache/EmailProviderCacheTest.java | 12 +++++++----- .../controller/MessagingControllerTest.java | 2 +- .../com/fsck/k9/helper/MessageHelperTest.java | 2 +- .../MessageViewInfoExtractorTest.java | 2 +- .../AttachmentInfoExtractorTest.java | 2 +- .../k9/preferences/SettingsExporterTest.kt | 2 +- .../k9/preferences/SettingsImporterTest.java | 19 +++++++++++-------- .../com/fsck/k9/DependencyInjectionTest.kt | 4 ++-- .../unread/UnreadWidgetDataProviderTest.kt | 2 +- .../k9/preferences/StoragePersisterTest.kt | 2 +- .../k9/storage/StoreSchemaDefinitionTest.java | 2 +- .../activity/compose/RecipientLoaderTest.java | 2 +- .../fsck/k9/message/PgpMessageBuilderTest.kt | 2 +- .../test/java/com/fsck/k9/ui/K9DrawerTest.kt | 2 +- .../k9/ui/crypto/MessageCryptoHelperTest.java | 2 +- .../helper/RelativeDateTimeFormatterTest.kt | 2 +- .../fsck/k9/ui/helper/SizeFormatterTest.kt | 2 +- 18 files changed, 35 insertions(+), 30 deletions(-) diff --git a/app/core/src/test/java/com/fsck/k9/autocrypt/AutocryptHeaderParserTest.java b/app/core/src/test/java/com/fsck/k9/autocrypt/AutocryptHeaderParserTest.java index 283d0841f3..56f45561bf 100644 --- a/app/core/src/test/java/com/fsck/k9/autocrypt/AutocryptHeaderParserTest.java +++ b/app/core/src/test/java/com/fsck/k9/autocrypt/AutocryptHeaderParserTest.java @@ -23,7 +23,7 @@ public class AutocryptHeaderParserTest extends RobolectricTest { @Before public void setUp() throws Exception { - BinaryTempFileBody.setTempDirectory(RuntimeEnvironment.application.getCacheDir()); + BinaryTempFileBody.setTempDirectory(RuntimeEnvironment.getApplication().getCacheDir()); } // Test cases taken from: https://github.com/mailencrypt/autocrypt/tree/master/src/tests/data diff --git a/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.java b/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.java index 4c2b75ada2..e673c332b3 100644 --- a/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.java +++ b/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.java @@ -4,6 +4,7 @@ package com.fsck.k9.cache; import java.util.Collections; import java.util.UUID; +import android.content.Context; import android.net.Uri; import com.fsck.k9.RobolectricTest; @@ -26,6 +27,7 @@ import static org.mockito.Mockito.when; public class EmailProviderCacheTest extends RobolectricTest { + private final Context context = RuntimeEnvironment.getApplication(); private EmailProviderCache cache; @Mock @@ -40,7 +42,7 @@ public class EmailProviderCacheTest extends RobolectricTest { MockitoAnnotations.initMocks(this); EmailProvider.CONTENT_URI = Uri.parse("content://test.provider.email"); - cache = EmailProviderCache.getCache(UUID.randomUUID().toString(), RuntimeEnvironment.application); + cache = EmailProviderCache.getCache(UUID.randomUUID().toString(), context); when(mockLocalMessage.getDatabaseId()).thenReturn(localMessageId); when(mockLocalMessage.getFolder()).thenReturn(mockLocalMessageFolder); when(mockLocalMessageFolder.getDatabaseId()).thenReturn(localMessageFolderId); @@ -48,16 +50,16 @@ public class EmailProviderCacheTest extends RobolectricTest { @Test public void getCache_returnsDifferentCacheForEachUUID() { - EmailProviderCache cache = EmailProviderCache.getCache("u001", RuntimeEnvironment.application); - EmailProviderCache cache2 = EmailProviderCache.getCache("u002", RuntimeEnvironment.application); + EmailProviderCache cache = EmailProviderCache.getCache("u001", context); + EmailProviderCache cache2 = EmailProviderCache.getCache("u002", context); assertNotEquals(cache, cache2); } @Test public void getCache_returnsSameCacheForAUUID() { - EmailProviderCache cache = EmailProviderCache.getCache("u001", RuntimeEnvironment.application); - EmailProviderCache cache2 = EmailProviderCache.getCache("u001", RuntimeEnvironment.application); + EmailProviderCache cache = EmailProviderCache.getCache("u001", context); + EmailProviderCache cache2 = EmailProviderCache.getCache("u001", context); assertSame(cache, cache2); } diff --git a/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java b/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java index b1d6821f39..641dbc62e0 100644 --- a/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java +++ b/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java @@ -137,7 +137,7 @@ public class MessagingControllerTest extends K9RobolectricTest { public void setUp() throws MessagingException { ShadowLog.stream = System.out; MockitoAnnotations.initMocks(this); - appContext = RuntimeEnvironment.application; + appContext = RuntimeEnvironment.getApplication(); preferences = Preferences.getPreferences(); diff --git a/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.java b/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.java index 85347a3220..9f003be378 100644 --- a/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.java +++ b/app/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.java @@ -22,7 +22,7 @@ public class MessageHelperTest extends RobolectricTest { @Before public void setUp() throws Exception { - Context context = RuntimeEnvironment.application; + Context context = RuntimeEnvironment.getApplication(); contacts = new Contacts(context); contactsWithFakeContact = new Contacts(context) { @Override public String getNameForAddress(String address) { diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java b/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java index 65a65de70d..16fce8e4de 100644 --- a/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java +++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageViewInfoExtractorTest.java @@ -73,7 +73,7 @@ public class MessageViewInfoExtractorTest extends K9RobolectricTest { @Before public void setUp() throws Exception { - context = RuntimeEnvironment.application; + context = RuntimeEnvironment.getApplication(); HtmlProcessor htmlProcessor = createFakeHtmlProcessor(); attachmentInfoExtractor = spy(DI.get(AttachmentInfoExtractor.class)); diff --git a/app/core/src/test/java/com/fsck/k9/message/extractors/AttachmentInfoExtractorTest.java b/app/core/src/test/java/com/fsck/k9/message/extractors/AttachmentInfoExtractorTest.java index 3fa4896e37..c83476a9b2 100644 --- a/app/core/src/test/java/com/fsck/k9/message/extractors/AttachmentInfoExtractorTest.java +++ b/app/core/src/test/java/com/fsck/k9/message/extractors/AttachmentInfoExtractorTest.java @@ -41,7 +41,7 @@ public class AttachmentInfoExtractorTest extends RobolectricTest { @Before public void setUp() throws Exception { AttachmentProvider.CONTENT_URI = Uri.parse("content://test.attachmentprovider"); - context = RuntimeEnvironment.application; + context = RuntimeEnvironment.getApplication(); attachmentInfoExtractor = new AttachmentInfoExtractor(context); } diff --git a/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt b/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt index e3177f8256..5819c1ad89 100644 --- a/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt +++ b/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt @@ -15,7 +15,7 @@ import org.mockito.kotlin.mock import org.robolectric.RuntimeEnvironment class SettingsExporterTest : K9RobolectricTest() { - private val contentResolver = RuntimeEnvironment.application.contentResolver + private val contentResolver = RuntimeEnvironment.getApplication().contentResolver private val preferences: Preferences by inject() private val folderSettingsProvider: FolderSettingsProvider by inject() private val folderRepository: FolderRepository by inject() diff --git a/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java b/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java index 6b9be20b83..50504e5c21 100644 --- a/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java +++ b/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java @@ -6,6 +6,8 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +import android.content.Context; + import com.fsck.k9.K9RobolectricTest; import com.fsck.k9.Preferences; import com.fsck.k9.mail.AuthType; @@ -20,6 +22,7 @@ import static org.junit.Assert.assertTrue; public class SettingsImporterTest extends K9RobolectricTest { + private final Context context = RuntimeEnvironment.getApplication(); @Before public void before() { @@ -36,7 +39,7 @@ public class SettingsImporterTest extends K9RobolectricTest { InputStream inputStream = inputStreamOf(""); List accountUuids = new ArrayList<>(); - SettingsImporter.importSettings(RuntimeEnvironment.application, inputStream, true, accountUuids, true); + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); } @Test(expected = SettingsImportExportException.class) @@ -44,7 +47,7 @@ public class SettingsImporterTest extends K9RobolectricTest { InputStream inputStream = inputStreamOf(""); List accountUuids = new ArrayList<>(); - SettingsImporter.importSettings(RuntimeEnvironment.application, inputStream, true, accountUuids, true); + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); } @Test(expected = SettingsImportExportException.class) @@ -52,7 +55,7 @@ public class SettingsImporterTest extends K9RobolectricTest { InputStream inputStream = inputStreamOf(""); List accountUuids = new ArrayList<>(); - SettingsImporter.importSettings(RuntimeEnvironment.application, inputStream, true, accountUuids, true); + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); } @Test(expected = SettingsImportExportException.class) @@ -60,7 +63,7 @@ public class SettingsImporterTest extends K9RobolectricTest { InputStream inputStream = inputStreamOf(""); List accountUuids = new ArrayList<>(); - SettingsImporter.importSettings(RuntimeEnvironment.application, inputStream, true, accountUuids, true); + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); } @Test(expected = SettingsImportExportException.class) @@ -68,7 +71,7 @@ public class SettingsImporterTest extends K9RobolectricTest { InputStream inputStream = inputStreamOf(""); List accountUuids = new ArrayList<>(); - SettingsImporter.importSettings(RuntimeEnvironment.application, inputStream, true, accountUuids, true); + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); } @Test(expected = SettingsImportExportException.class) @@ -76,7 +79,7 @@ public class SettingsImporterTest extends K9RobolectricTest { InputStream inputStream = inputStreamOf(""); List accountUuids = new ArrayList<>(); - SettingsImporter.importSettings(RuntimeEnvironment.application, inputStream, true, accountUuids, true); + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); } @Test(expected = SettingsImportExportException.class) @@ -84,7 +87,7 @@ public class SettingsImporterTest extends K9RobolectricTest { InputStream inputStream = inputStreamOf(""); List accountUuids = new ArrayList<>(); - SettingsImporter.importSettings(RuntimeEnvironment.application, inputStream, true, accountUuids, true); + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); } @Test @@ -162,7 +165,7 @@ public class SettingsImporterTest extends K9RobolectricTest { accountUuids.add(validUUID); SettingsImporter.ImportResults results = SettingsImporter.importSettings( - RuntimeEnvironment.application, inputStream, true, accountUuids, false); + context, inputStream, true, accountUuids, false); assertEquals(0, results.erroneousAccounts.size()); assertEquals(1, results.importedAccounts.size()); diff --git a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt index 14a763039c..db007db4da 100644 --- a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt +++ b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt @@ -39,8 +39,8 @@ class DependencyInjectionTest : AutoCloseKoinTest() { getKoin().checkModules { withParameter { lifecycleOwner } create { parametersOf(lifecycleOwner, autocryptTransferView) } - withParameter { RuntimeEnvironment.application } - withParameter { RuntimeEnvironment.application } + withParameter { RuntimeEnvironment.getApplication() } + withParameter { RuntimeEnvironment.getApplication() } withParameter { ChangeLogMode.CHANGE_LOG } } } diff --git a/app/k9mail/src/test/java/com/fsck/k9/widget/unread/UnreadWidgetDataProviderTest.kt b/app/k9mail/src/test/java/com/fsck/k9/widget/unread/UnreadWidgetDataProviderTest.kt index a724a86cbc..d45da6c52c 100644 --- a/app/k9mail/src/test/java/com/fsck/k9/widget/unread/UnreadWidgetDataProviderTest.kt +++ b/app/k9mail/src/test/java/com/fsck/k9/widget/unread/UnreadWidgetDataProviderTest.kt @@ -21,7 +21,7 @@ import org.mockito.kotlin.mock import org.robolectric.RuntimeEnvironment class UnreadWidgetDataProviderTest : AppRobolectricTest() { - val context: Context = RuntimeEnvironment.application + val context: Context = RuntimeEnvironment.getApplication() val account = createAccount() val preferences = createPreferences() val messagingController = createMessagingController() diff --git a/app/storage/src/test/java/com/fsck/k9/preferences/StoragePersisterTest.kt b/app/storage/src/test/java/com/fsck/k9/preferences/StoragePersisterTest.kt index 005f997393..95898e16dc 100644 --- a/app/storage/src/test/java/com/fsck/k9/preferences/StoragePersisterTest.kt +++ b/app/storage/src/test/java/com/fsck/k9/preferences/StoragePersisterTest.kt @@ -18,7 +18,7 @@ import org.mockito.kotlin.verifyNoMoreInteractions import org.robolectric.RuntimeEnvironment class StoragePersisterTest : K9RobolectricTest() { - private var context: Context = RuntimeEnvironment.application + private var context: Context = RuntimeEnvironment.getApplication() private var storagePersister = K9StoragePersister(context) @Test diff --git a/app/storage/src/test/java/com/fsck/k9/storage/StoreSchemaDefinitionTest.java b/app/storage/src/test/java/com/fsck/k9/storage/StoreSchemaDefinitionTest.java index 7d103d3378..325eac1965 100644 --- a/app/storage/src/test/java/com/fsck/k9/storage/StoreSchemaDefinitionTest.java +++ b/app/storage/src/test/java/com/fsck/k9/storage/StoreSchemaDefinitionTest.java @@ -45,7 +45,7 @@ public class StoreSchemaDefinitionTest extends K9RobolectricTest { public void setUp() throws MessagingException { ShadowLog.stream = System.out; - Application application = RuntimeEnvironment.application; + Application application = RuntimeEnvironment.getApplication(); StorageManager.getInstance(application); storeSchemaDefinition = createStoreSchemaDefinition(); diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/activity/compose/RecipientLoaderTest.java b/app/ui/legacy/src/test/java/com/fsck/k9/activity/compose/RecipientLoaderTest.java index e2753934a0..6d98945522 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/activity/compose/RecipientLoaderTest.java +++ b/app/ui/legacy/src/test/java/com/fsck/k9/activity/compose/RecipientLoaderTest.java @@ -77,7 +77,7 @@ public class RecipientLoaderTest extends RobolectricTest { @Before public void setUp() throws Exception { - Application application = RuntimeEnvironment.application; + Application application = RuntimeEnvironment.getApplication(); shadowApp = Shadows.shadowOf(application); shadowApp.grantPermissions(Manifest.permission.READ_CONTACTS); shadowApp.grantPermissions(Manifest.permission.WRITE_CONTACTS); diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt index b0f96c5dae..c4887fb772 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt @@ -81,7 +81,7 @@ class PgpMessageBuilderTest : K9RobolectricTest() { @Before @Throws(Exception::class) fun setUp() { - BinaryTempFileBody.setTempDirectory(RuntimeEnvironment.application.cacheDir) + BinaryTempFileBody.setTempDirectory(RuntimeEnvironment.getApplication().cacheDir) `when`(autocryptOpenPgpApiInteractor.getKeyMaterialForKeyId(openPgpApi, TEST_KEY_ID, SENDER_EMAIL)) .thenReturn(AUTOCRYPT_KEY_MATERIAL) } diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/ui/K9DrawerTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/ui/K9DrawerTest.kt index 3ffd67391f..48c04348c1 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/ui/K9DrawerTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/ui/K9DrawerTest.kt @@ -8,7 +8,7 @@ import org.robolectric.RuntimeEnvironment class K9DrawerTest : RobolectricTest() { @Test fun testAccountColorLengthEqualsDrawerColorLength() { - val resources = RuntimeEnvironment.application.resources + val resources = RuntimeEnvironment.getApplication().resources val lightColors = resources.getIntArray(R.array.account_colors) val darkColors = resources.getIntArray(R.array.drawer_account_accent_color_dark_theme) diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/ui/crypto/MessageCryptoHelperTest.java b/app/ui/legacy/src/test/java/com/fsck/k9/ui/crypto/MessageCryptoHelperTest.java index 86d08c9149..42b3d36a09 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/ui/crypto/MessageCryptoHelperTest.java +++ b/app/ui/legacy/src/test/java/com/fsck/k9/ui/crypto/MessageCryptoHelperTest.java @@ -70,7 +70,7 @@ public class MessageCryptoHelperTest extends RobolectricTest { when(openPgpApiFactory.createOpenPgpApi(any(Context.class), nullable(IOpenPgpService2.class))) .thenReturn(openPgpApi); - messageCryptoHelper = new MessageCryptoHelper(RuntimeEnvironment.application, openPgpApiFactory, + messageCryptoHelper = new MessageCryptoHelper(RuntimeEnvironment.getApplication(), openPgpApiFactory, autocryptOperations, "org.example.dummy"); messageCryptoCallback = mock(MessageCryptoCallback.class); } diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatterTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatterTest.kt index f0258c304a..b38bf13634 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatterTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatterTest.kt @@ -16,7 +16,7 @@ import org.robolectric.annotation.Config @Config(qualifiers = "en") class RelativeDateTimeFormatterTest : RobolectricTest() { - private val context = RuntimeEnvironment.application.applicationContext + private val context = RuntimeEnvironment.getApplication().applicationContext private val clock = TestClock() private val dateTimeFormatter = RelativeDateTimeFormatter(context, clock) diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/SizeFormatterTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/SizeFormatterTest.kt index 9c009965ae..9af9595784 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/SizeFormatterTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/ui/helper/SizeFormatterTest.kt @@ -8,7 +8,7 @@ import org.robolectric.annotation.Config @Config(qualifiers = "en") class SizeFormatterTest : RobolectricTest() { - private val sizeFormatter = SizeFormatter(RuntimeEnvironment.application.resources) + private val sizeFormatter = SizeFormatter(RuntimeEnvironment.getApplication().resources) @Test fun bytes_lower_bound() { -- GitLab From 72e3ee3a21eb5f18bda12809fc340bcfc7c2f0c5 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Aug 2022 18:43:00 +0200 Subject: [PATCH 19/85] Clean up `DependencyInjectionTest` --- .../src/test/java/com/fsck/k9/DependencyInjectionTest.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt index db007db4da..f9da2d72ca 100644 --- a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt +++ b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt @@ -1,6 +1,5 @@ package com.fsck.k9 -import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import com.fsck.k9.ui.changelog.ChangeLogMode import com.fsck.k9.ui.changelog.ChangelogViewModel @@ -26,10 +25,10 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(application = App::class) class DependencyInjectionTest : AutoCloseKoinTest() { - val lifecycleOwner = mock { - on { lifecycle } doReturn mock() + private val lifecycleOwner = mock { + on { lifecycle } doReturn mock() } - val autocryptTransferView = mock() + private val autocryptTransferView = mock() @KoinInternalApi @Test @@ -38,7 +37,7 @@ class DependencyInjectionTest : AutoCloseKoinTest() { getKoin().checkModules { withParameter { lifecycleOwner } - create { parametersOf(lifecycleOwner, autocryptTransferView) } + withParameters { parametersOf(lifecycleOwner, autocryptTransferView) } withParameter { RuntimeEnvironment.getApplication() } withParameter { RuntimeEnvironment.getApplication() } withParameter { ChangeLogMode.CHANGE_LOG } -- GitLab From a039b1ae514466c01009ed73b13d3e8528cd3005 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Aug 2022 15:08:17 +0200 Subject: [PATCH 20/85] Rename .java to .kt --- .../store/imap/{SimpleImapSettings.java => SimpleImapSettings.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/{SimpleImapSettings.java => SimpleImapSettings.kt} (100%) diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt similarity index 100% rename from mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.java rename to mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt -- GitLab From 99f6e42b28dd8795d7c43e2283840440f1e522de Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Aug 2022 15:08:17 +0200 Subject: [PATCH 21/85] Convert `SimpleImapSettings` to Kotlin --- .../mail/store/imap/RealImapConnectionTest.kt | 75 +++++++---- .../k9/mail/store/imap/SimpleImapSettings.kt | 125 +++++------------- 2 files changed, 79 insertions(+), 121 deletions(-) diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt index ef4fc9e1e4..9a404ff48a 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt @@ -38,10 +38,6 @@ private val OAUTHBEARER_STRING = "n,a=$USERNAME,\u0001auth=Bearer $XOAUTH_TOKEN\ class RealImapConnectionTest { private var socketFactory = TestTrustedSocketFactory.newInstance() private var oAuth2TokenProvider = TestTokenProvider() - private var settings = SimpleImapSettings().apply { - username = USERNAME - password = PASSWORD - } @Before fun setUp() { @@ -646,8 +642,7 @@ class RealImapConnectionTest { @Test fun `open() with connection error should throw`() { - settings.host = "127.1.2.3" - settings.port = 143 + val settings = createImapSettings(host = "127.1.2.3") val imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider) try { @@ -661,8 +656,7 @@ class RealImapConnectionTest { @Test fun `open() with invalid hostname should throw`() { - settings.host = "host name" - settings.port = 143 + val settings = createImapSettings(host = "host name") val imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider) try { @@ -676,7 +670,6 @@ class RealImapConnectionTest { @Test fun `open() with STARTTLS capability should issue STARTTLS command`() { - settings.connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED val server = MockImapServer().apply { preAuthenticationDialog(capabilities = "STARTTLS LOGINDISABLED") expect("2 STARTTLS") @@ -691,7 +684,11 @@ class RealImapConnectionTest { output("* NAMESPACE ((\"\" \"/\")) NIL NIL") output("5 OK command completed") } - val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) + val imapConnection = startServerAndCreateImapConnection( + server, + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED, + authType = AuthType.PLAIN + ) imapConnection.open() @@ -701,11 +698,13 @@ class RealImapConnectionTest { @Test fun `open() with STARTTLS but without STARTTLS capability should throw`() { - settings.connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED val server = MockImapServer().apply { preAuthenticationDialog() } - val imapConnection = startServerAndCreateImapConnection(server) + val imapConnection = startServerAndCreateImapConnection( + server, + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED, + ) try { imapConnection.open() @@ -721,7 +720,6 @@ class RealImapConnectionTest { @Test fun `open() with untagged CAPABILITY after STARTTLS should not throw`() { - settings.connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED val server = MockImapServer().apply { preAuthenticationDialog(capabilities = "STARTTLS LOGINDISABLED") expect("2 STARTTLS") @@ -735,7 +733,11 @@ class RealImapConnectionTest { output("4 OK [CAPABILITY IMAP4REV1] LOGIN completed") simplePostAuthenticationDialog(tag = 5) } - val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) + val imapConnection = startServerAndCreateImapConnection( + server, + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED, + authType = AuthType.PLAIN + ) imapConnection.open() @@ -745,13 +747,16 @@ class RealImapConnectionTest { @Test fun `open() with negative response to STARTTLS command should throw`() { - settings.connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED val server = MockImapServer().apply { preAuthenticationDialog(capabilities = "STARTTLS") expect("2 STARTTLS") output("2 NO") } - val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) + val imapConnection = startServerAndCreateImapConnection( + server, + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED, + authType = AuthType.PLAIN + ) try { imapConnection.open() @@ -766,7 +771,6 @@ class RealImapConnectionTest { @Test fun `open() with COMPRESS=DEFLATE capability should enable compression`() { - settings.setUseCompression(true) val server = MockImapServer().apply { simplePreAuthAndLoginDialog(postAuthCapabilities = "COMPRESS=DEFLATE") expect("3 COMPRESS DEFLATE") @@ -774,7 +778,7 @@ class RealImapConnectionTest { enableCompression() simplePostAuthenticationDialog(tag = 4) } - val imapConnection = startServerAndCreateImapConnection(server) + val imapConnection = startServerAndCreateImapConnection(server, useCompression = true) imapConnection.open() @@ -784,14 +788,13 @@ class RealImapConnectionTest { @Test fun `open() with negative response to COMPRESS command should continue`() { - settings.setUseCompression(true) val server = MockImapServer().apply { simplePreAuthAndLoginDialog(postAuthCapabilities = "COMPRESS=DEFLATE") expect("3 COMPRESS DEFLATE") output("3 NO") simplePostAuthenticationDialog(tag = 4) } - val imapConnection = startServerAndCreateImapConnection(server) + val imapConnection = startServerAndCreateImapConnection(server, useCompression = true) imapConnection.open() @@ -801,13 +804,12 @@ class RealImapConnectionTest { @Test fun `open() with IOException during COMPRESS command should throw`() { - settings.setUseCompression(true) val server = MockImapServer().apply { simplePreAuthAndLoginDialog(postAuthCapabilities = "COMPRESS=DEFLATE") expect("3 COMPRESS DEFLATE") closeConnection() } - val imapConnection = startServerAndCreateImapConnection(server) + val imapConnection = startServerAndCreateImapConnection(server, useCompression = true) try { imapConnection.open() @@ -855,6 +857,7 @@ class RealImapConnectionTest { @Test fun `isConnected without previous open() should return false`() { + val settings = createImapSettings() val imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider) val result = imapConnection.isConnected @@ -889,6 +892,7 @@ class RealImapConnectionTest { @Test fun `close() without open() should not throw`() { + val settings = createImapSettings() val imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider) imapConnection.close() @@ -1015,12 +1019,21 @@ class RealImapConnectionTest { private fun startServerAndCreateImapConnection( server: MockImapServer, - authType: AuthType = AuthType.PLAIN + connectionSecurity: ConnectionSecurity = ConnectionSecurity.NONE, + authType: AuthType = AuthType.PLAIN, + useCompression: Boolean = false ): ImapConnection { server.start() - settings.host = server.host - settings.port = server.port - settings.authType = authType + + val settings = SimpleImapSettings( + host = server.host, + port = server.port, + connectionSecurity = connectionSecurity, + authType = authType, + username = USERNAME, + password = PASSWORD, + useCompression = useCompression + ) return createImapConnection(settings, socketFactory, oAuth2TokenProvider) } @@ -1068,11 +1081,19 @@ class RealImapConnectionTest { } private fun MockImapServer.simplePreAuthAndLoginDialog(postAuthCapabilities: String = "") { - settings.authType = AuthType.PLAIN preAuthenticationDialog() expect("2 LOGIN \"$USERNAME\" \"$PASSWORD\"") output("2 OK [CAPABILITY $postAuthCapabilities] LOGIN completed") } + + private fun createImapSettings(host: String = "irrelevant"): ImapSettings { + return SimpleImapSettings( + host = host, + port = 143, + authType = AuthType.PLAIN, + username = "irrelevant" + ) + } } class TestTokenProvider : OAuth2TokenProvider { diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt index 391ae35d8a..66c1d34872 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt @@ -1,113 +1,50 @@ -package com.fsck.k9.mail.store.imap; +package com.fsck.k9.mail.store.imap +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.ConnectionSecurity -import com.fsck.k9.mail.AuthType; -import com.fsck.k9.mail.ConnectionSecurity; +internal class SimpleImapSettings( + private val host: String, + private val port: Int = 0, + private val connectionSecurity: ConnectionSecurity = ConnectionSecurity.NONE, + private val authType: AuthType, + private val username: String, + private val password: String? = null, + private val useCompression: Boolean = false +) : ImapSettings { + private var pathPrefix: String? = null + private var pathDelimiter: String? = null + private var combinedPrefix: String? = null + override fun getHost(): String = host -class SimpleImapSettings implements ImapSettings { - private String host; - private int port; - private ConnectionSecurity connectionSecurity = ConnectionSecurity.NONE; - private AuthType authType; - private String username; - private String password; - private String pathPrefix; - private String pathDelimiter; - private String combinedPrefix; - private boolean useCompression = false; + override fun getPort(): Int = port + override fun getConnectionSecurity(): ConnectionSecurity = connectionSecurity - @Override - public String getHost() { - return host; - } - - @Override - public int getPort() { - return port; - } - - @Override - public ConnectionSecurity getConnectionSecurity() { - return connectionSecurity; - } - - @Override - public AuthType getAuthType() { - return authType; - } + override fun getAuthType(): AuthType = authType - @Override - public String getUsername() { - return username; - } + override fun getUsername(): String = username - @Override - public String getPassword() { - return password; - } + override fun getPassword(): String? = password - @Override - public String getClientCertificateAlias() { - return null; - } + override fun getClientCertificateAlias(): String? = null - @Override - public boolean useCompression() { - return useCompression; - } + override fun useCompression(): Boolean = useCompression - @Override - public String getPathPrefix() { - return pathPrefix; - } - - @Override - public void setPathPrefix(String prefix) { - pathPrefix = prefix; - } + override fun getPathPrefix(): String? = pathPrefix - @Override - public String getPathDelimiter() { - return pathDelimiter; + override fun setPathPrefix(prefix: String?) { + pathPrefix = prefix } - @Override - public void setPathDelimiter(String delimiter) { - pathDelimiter = delimiter; - } - - @Override - public void setCombinedPrefix(String prefix) { - combinedPrefix = prefix; - } - - void setHost(String host) { - this.host = host; - } - - void setPort(int port) { - this.port = port; - } - - void setConnectionSecurity(ConnectionSecurity connectionSecurity) { - this.connectionSecurity = connectionSecurity; - } - - void setAuthType(AuthType authType) { - this.authType = authType; - } - - void setUsername(String username) { - this.username = username; - } + override fun getPathDelimiter(): String? = pathDelimiter - void setPassword(String password) { - this.password = password; + override fun setPathDelimiter(delimiter: String?) { + pathDelimiter = delimiter } - void setUseCompression(boolean useCompression) { - this.useCompression = useCompression; + override fun setCombinedPrefix(prefix: String?) { + combinedPrefix = prefix } } -- GitLab From 01539af98549cf29e2b0136c604f86e3d96f9dc4 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Aug 2022 15:17:41 +0200 Subject: [PATCH 22/85] Rename .java to .kt --- .../k9/mail/store/imap/{ImapSettings.java => ImapSettings.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/{ImapSettings.java => ImapSettings.kt} (100%) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt similarity index 100% rename from mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.java rename to mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt -- GitLab From b90fc50d1afddd05d99e7a4082edf28652e13c26 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Aug 2022 15:17:41 +0200 Subject: [PATCH 23/85] Convert `ImapSettings` to Kotlin --- .../fsck/k9/mail/store/imap/ImapSettings.kt | 48 +++++++------------ .../k9/mail/store/imap/SimpleImapSettings.kt | 48 ++++--------------- 2 files changed, 27 insertions(+), 69 deletions(-) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt index d5c5e1efba..b5cf301975 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt @@ -1,36 +1,22 @@ -package com.fsck.k9.mail.store.imap; - -import com.fsck.k9.mail.AuthType; -import com.fsck.k9.mail.ConnectionSecurity; +package com.fsck.k9.mail.store.imap +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.ConnectionSecurity /** - * Settings source for IMAP. Implemented in order to remove coupling between {@link ImapStore} and {@link ImapConnection}. + * Settings source for IMAP. Implemented in order to remove coupling between [ImapStore] and [ImapConnection]. */ -interface ImapSettings { - String getHost(); - - int getPort(); - - ConnectionSecurity getConnectionSecurity(); - - AuthType getAuthType(); - - String getUsername(); - - String getPassword(); - - String getClientCertificateAlias(); - - boolean useCompression(); - - String getPathPrefix(); - - void setPathPrefix(String prefix); - - String getPathDelimiter(); - - void setPathDelimiter(String delimiter); - - void setCombinedPrefix(String prefix); +internal interface ImapSettings { + val host: String + val port: Int + val connectionSecurity: ConnectionSecurity + val authType: AuthType + val username: String + val password: String? + val clientCertificateAlias: String? + fun useCompression(): Boolean + + var pathPrefix: String? + var pathDelimiter: String? + fun setCombinedPrefix(prefix: String?) } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt index 66c1d34872..5a74db0b92 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt @@ -4,47 +4,19 @@ import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.ConnectionSecurity internal class SimpleImapSettings( - private val host: String, - private val port: Int = 0, - private val connectionSecurity: ConnectionSecurity = ConnectionSecurity.NONE, - private val authType: AuthType, - private val username: String, - private val password: String? = null, + override val host: String, + override val port: Int = 0, + override val connectionSecurity: ConnectionSecurity = ConnectionSecurity.NONE, + override val authType: AuthType, + override val username: String, + override val password: String? = null, private val useCompression: Boolean = false ) : ImapSettings { - private var pathPrefix: String? = null - private var pathDelimiter: String? = null - private var combinedPrefix: String? = null - - override fun getHost(): String = host - - override fun getPort(): Int = port - - override fun getConnectionSecurity(): ConnectionSecurity = connectionSecurity - - override fun getAuthType(): AuthType = authType - - override fun getUsername(): String = username - - override fun getPassword(): String? = password - - override fun getClientCertificateAlias(): String? = null - + override val clientCertificateAlias: String? = null override fun useCompression(): Boolean = useCompression - override fun getPathPrefix(): String? = pathPrefix - - override fun setPathPrefix(prefix: String?) { - pathPrefix = prefix - } - - override fun getPathDelimiter(): String? = pathDelimiter - - override fun setPathDelimiter(delimiter: String?) { - pathDelimiter = delimiter - } + override var pathPrefix: String? = null + override var pathDelimiter: String? = null - override fun setCombinedPrefix(prefix: String?) { - combinedPrefix = prefix - } + override fun setCombinedPrefix(prefix: String?) = Unit } -- GitLab From 33fa9d2b7c4e7b32d09d9b49e954d9f9faff55f8 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Aug 2022 16:19:21 +0200 Subject: [PATCH 24/85] Rename .java to .kt --- .../k9/mail/store/imap/{RealImapStore.java => RealImapStore.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/{RealImapStore.java => RealImapStore.kt} (100%) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt similarity index 100% rename from mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java rename to mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt -- GitLab From a57f127a11e035f6863253f959019ca44e0fe907 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Aug 2022 16:19:21 +0200 Subject: [PATCH 25/85] Convert `RealImapStore` to Kotlin --- .../fsck/k9/mail/store/imap/RealImapStore.kt | 541 ++++++++---------- .../k9/mail/store/imap/RealImapStoreTest.java | 2 +- 2 files changed, 227 insertions(+), 316 deletions(-) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt index 947cc7732b..9d67714a20 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt @@ -1,411 +1,322 @@ -package com.fsck.k9.mail.store.imap; - - -import java.io.IOException; -import java.nio.charset.CharacterCodingException; -import java.util.ArrayList; -import java.util.Deque; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.fsck.k9.logging.Timber; -import com.fsck.k9.mail.AuthType; -import com.fsck.k9.mail.AuthenticationFailedException; -import com.fsck.k9.mail.ConnectionSecurity; -import com.fsck.k9.mail.Flag; -import com.fsck.k9.mail.FolderType; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.ServerSettings; -import com.fsck.k9.mail.oauth.OAuth2TokenProvider; -import com.fsck.k9.mail.ssl.TrustedSocketFactory; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - - -/** - *
- * TODO Need a default response handler for things like folder updates
- * 
- */ -class RealImapStore implements ImapStore, ImapConnectionManager, InternalImapStore { - private final ImapStoreConfig config; - private final TrustedSocketFactory trustedSocketFactory; - private Set permanentFlagsIndex = EnumSet.noneOf(Flag.class); - private OAuth2TokenProvider oauthTokenProvider; - - private String host; - private int port; - private String username; - private String password; - private String clientCertificateAlias; - private ConnectionSecurity connectionSecurity; - private AuthType authType; - private String pathPrefix; - private String combinedPrefix = null; - private String pathDelimiter = null; - private final Deque connections = new LinkedList<>(); - private FolderNameCodec folderNameCodec; - private volatile int connectionGeneration = 1; - - - public RealImapStore(ServerSettings serverSettings, ImapStoreConfig config, - TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oauthTokenProvider) { - this.config = config; - this.trustedSocketFactory = trustedSocketFactory; - - host = serverSettings.host; - port = serverSettings.port; - - connectionSecurity = serverSettings.connectionSecurity; - this.oauthTokenProvider = oauthTokenProvider; - - authType = serverSettings.authenticationType; - username = serverSettings.username; - password = serverSettings.password; - clientCertificateAlias = serverSettings.clientCertificateAlias; - - boolean autoDetectNamespace = ImapStoreSettings.getAutoDetectNamespace(serverSettings); - String pathPrefixSetting = ImapStoreSettings.getPathPrefix(serverSettings); +package com.fsck.k9.mail.store.imap + +import com.fsck.k9.logging.Timber +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.AuthenticationFailedException +import com.fsck.k9.mail.ConnectionSecurity +import com.fsck.k9.mail.Flag +import com.fsck.k9.mail.FolderType +import com.fsck.k9.mail.MessagingException +import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.ssl.TrustedSocketFactory +import com.fsck.k9.mail.store.imap.ImapStoreSettings.autoDetectNamespace +import com.fsck.k9.mail.store.imap.ImapStoreSettings.pathPrefix +import java.io.IOException +import java.util.Deque +import java.util.LinkedList + +internal open class RealImapStore( + private val serverSettings: ServerSettings, + private val config: ImapStoreConfig, + private val trustedSocketFactory: TrustedSocketFactory, + private val oauthTokenProvider: OAuth2TokenProvider? +) : ImapStore, ImapConnectionManager, InternalImapStore { + private val folderNameCodec: FolderNameCodec = FolderNameCodec.newInstance() + + private val host: String = checkNotNull(serverSettings.host) + + private var pathPrefix: String? + private var combinedPrefix: String? = null + private var pathDelimiter: String? = null + + private val permanentFlagsIndex: MutableSet = mutableSetOf() + private val connections: Deque = LinkedList() + + @Volatile + private var connectionGeneration = 1 + + init { + val autoDetectNamespace = serverSettings.autoDetectNamespace + val pathPrefixSetting = serverSettings.pathPrefix // Make extra sure pathPrefix is null if "auto-detect namespace" is configured - pathPrefix = autoDetectNamespace ? null : pathPrefixSetting; + pathPrefix = if (autoDetectNamespace) null else pathPrefixSetting + } - folderNameCodec = FolderNameCodec.newInstance(); + override fun getFolder(name: String): ImapFolder { + return RealImapFolder( + internalImapStore = this, + connectionManager = this, + serverId = name, + folderNameCodec = folderNameCodec + ) } - public ImapFolder getFolder(String name) { - return new RealImapFolder(this, this, name, folderNameCodec); + override fun getCombinedPrefix(): String { + return combinedPrefix ?: buildCombinedPrefix().also { combinedPrefix = it } } - @Override - @NotNull - public String getCombinedPrefix() { - if (combinedPrefix == null) { - if (pathPrefix != null) { - String tmpPrefix = pathPrefix.trim(); - String tmpDelim = (pathDelimiter != null ? pathDelimiter.trim() : ""); - if (tmpPrefix.endsWith(tmpDelim)) { - combinedPrefix = tmpPrefix; - } else if (tmpPrefix.length() > 0) { - combinedPrefix = tmpPrefix + tmpDelim; - } else { - combinedPrefix = ""; - } - } else { - combinedPrefix = ""; - } - } + private fun buildCombinedPrefix(): String { + val pathPrefix = pathPrefix ?: return "" - return combinedPrefix; - } + val trimmedPathPrefix = pathPrefix.trim { it <= ' ' } + val trimmedPathDelimiter = pathDelimiter?.trim { it <= ' ' }.orEmpty() - public List getFolders() throws MessagingException { - ImapConnection connection = getConnection(); + return if (trimmedPathPrefix.endsWith(trimmedPathDelimiter)) { + trimmedPathPrefix + } else if (trimmedPathPrefix.isNotEmpty()) { + trimmedPathPrefix + trimmedPathDelimiter + } else { + "" + } + } - try { - List folders = listFolders(connection, false); + @Throws(MessagingException::class) + override fun getFolders(): List { + val connection = getConnection() + return try { + val folders = listFolders(connection, false) if (!config.isSubscribedFoldersOnly()) { - return folders; + return folders } - List subscribedFolders = listFolders(connection, true); - return limitToSubscribedFolders(folders, subscribedFolders); - } catch (AuthenticationFailedException e) { - connection.close(); - throw e; - } catch (IOException | MessagingException ioe) { - connection.close(); - throw new MessagingException("Unable to get folder list.", ioe); + val subscribedFolders = listFolders(connection, true) + limitToSubscribedFolders(folders, subscribedFolders) + } catch (e: AuthenticationFailedException) { + connection.close() + throw e + } catch (e: IOException) { + connection.close() + throw MessagingException("Unable to get folder list.", e) + } catch (e: MessagingException) { + connection.close() + throw MessagingException("Unable to get folder list.", e) } finally { - releaseConnection(connection); + releaseConnection(connection) } } - private List limitToSubscribedFolders(List folders, - List subscribedFolders) { - Set subscribedFolderNames = new HashSet<>(subscribedFolders.size()); - for (FolderListItem subscribedFolder : subscribedFolders) { - subscribedFolderNames.add(subscribedFolder.getServerId()); - } + private fun limitToSubscribedFolders( + folders: List, + subscribedFolders: List + ): List { + val subscribedFolderServerIds = subscribedFolders.map { it.serverId }.toSet() + return folders.filter { it.serverId in subscribedFolderServerIds } + } - List filteredFolders = new ArrayList<>(); - for (FolderListItem folder : folders) { - if (subscribedFolderNames.contains(folder.getServerId())) { - filteredFolders.add(folder); + @Throws(IOException::class, MessagingException::class) + private fun listFolders(connection: ImapConnection, subscribedOnly: Boolean): List { + val commandFormat = when { + subscribedOnly -> { + "LSUB \"\" %s" + } + connection.supportsListExtended -> { + "LIST \"\" %s RETURN (SPECIAL-USE)" + } + else -> { + "LIST \"\" %s" } } - return filteredFolders; - } - - private List listFolders(ImapConnection connection, boolean subscribedOnly) throws IOException, - MessagingException { + val encodedListPrefix = ImapUtility.encodeString(getCombinedPrefix() + "*") + val responses = connection.executeSimpleCommand(String.format(commandFormat, encodedListPrefix)) - String commandFormat; - if (subscribedOnly) { - commandFormat = "LSUB \"\" %s"; - } else if (connection.hasCapability(Capabilities.SPECIAL_USE) && - connection.hasCapability(Capabilities.LIST_EXTENDED)) { - commandFormat = "LIST \"\" %s RETURN (SPECIAL-USE)"; + val listResponses = if (subscribedOnly) { + ListResponse.parseLsub(responses) } else { - commandFormat = "LIST \"\" %s"; + ListResponse.parseList(responses) } - String encodedListPrefix = ImapUtility.encodeString(getCombinedPrefix() + "*"); - List responses = connection.executeSimpleCommand(String.format(commandFormat, encodedListPrefix)); - - List listResponses = (subscribedOnly) ? - ListResponse.parseLsub(responses) : - ListResponse.parseList(responses); - - Map folderMap = new HashMap<>(listResponses.size()); - for (ListResponse listResponse : listResponses) { - String serverId = listResponse.getName(); + val folderMap = mutableMapOf() + for (listResponse in listResponses) { + val serverId = listResponse.name if (pathDelimiter == null) { - pathDelimiter = listResponse.getHierarchyDelimiter(); - combinedPrefix = null; + pathDelimiter = listResponse.hierarchyDelimiter + combinedPrefix = null } - if (RealImapFolder.INBOX.equalsIgnoreCase(serverId)) { - continue; + if (RealImapFolder.INBOX.equals(serverId, ignoreCase = true)) { + continue } else if (listResponse.hasAttribute("\\NoSelect")) { - continue; + continue } - String name = getFolderDisplayName(serverId); - String oldServerId = getOldServerId(serverId); - - FolderType type; - if (listResponse.hasAttribute("\\Archive") || listResponse.hasAttribute("\\All")) { - type = FolderType.ARCHIVE; - } else if (listResponse.hasAttribute("\\Drafts")) { - type = FolderType.DRAFTS; - } else if (listResponse.hasAttribute("\\Sent")) { - type = FolderType.SENT; - } else if (listResponse.hasAttribute("\\Junk")) { - type = FolderType.SPAM; - } else if (listResponse.hasAttribute("\\Trash")) { - type = FolderType.TRASH; - } else { - type = FolderType.REGULAR; + val name = getFolderDisplayName(serverId) + val oldServerId = getOldServerId(serverId) + + val type = when { + listResponse.hasAttribute("\\Archive") -> FolderType.ARCHIVE + listResponse.hasAttribute("\\All") -> FolderType.ARCHIVE + listResponse.hasAttribute("\\Drafts") -> FolderType.DRAFTS + listResponse.hasAttribute("\\Sent") -> FolderType.SENT + listResponse.hasAttribute("\\Junk") -> FolderType.SPAM + listResponse.hasAttribute("\\Trash") -> FolderType.TRASH + else -> FolderType.REGULAR } - FolderListItem existingItem = folderMap.get(serverId); - if (existingItem == null || existingItem.getType() == FolderType.REGULAR) { - folderMap.put(serverId, new FolderListItem(serverId, name, type, oldServerId)); + val existingItem = folderMap[serverId] + if (existingItem == null || existingItem.type == FolderType.REGULAR) { + folderMap[serverId] = FolderListItem(serverId, name, type, oldServerId) } } - List folders = new ArrayList<>(folderMap.size() + 1); - folders.add(new FolderListItem(RealImapFolder.INBOX, RealImapFolder.INBOX, FolderType.INBOX, RealImapFolder.INBOX)); - folders.addAll(folderMap.values()); - - return folders; + return buildList { + add(FolderListItem(RealImapFolder.INBOX, RealImapFolder.INBOX, FolderType.INBOX, RealImapFolder.INBOX)) + addAll(folderMap.values) + } } - private String getFolderDisplayName(String serverId) { - String decodedFolderName; - try { - decodedFolderName = folderNameCodec.decode(serverId); - } catch (CharacterCodingException e) { - Timber.w(e, "Folder name not correctly encoded with the UTF-7 variant as defined by RFC 3501: %s", - serverId); - - decodedFolderName = serverId; + private fun getFolderDisplayName(serverId: String): String { + val decodedFolderName = try { + folderNameCodec.decode(serverId) + } catch (e: CharacterCodingException) { + Timber.w(e, "Folder name not correctly encoded with the UTF-7 variant as defined by RFC 3501: %s", serverId) + serverId } - String folderNameWithoutPrefix = removePrefixFromFolderName(decodedFolderName); - return folderNameWithoutPrefix != null ? folderNameWithoutPrefix : decodedFolderName; + val folderNameWithoutPrefix = removePrefixFromFolderName(decodedFolderName) + return folderNameWithoutPrefix ?: decodedFolderName } - @Nullable - private String getOldServerId(String serverId) { - String decodedFolderName; - try { - decodedFolderName = folderNameCodec.decode(serverId); - } catch (CharacterCodingException e) { + private fun getOldServerId(serverId: String): String? { + val decodedFolderName = try { + folderNameCodec.decode(serverId) + } catch (e: CharacterCodingException) { // Previous versions of K-9 Mail ignored folders with invalid UTF-7 encoding - return null; + return null } - return removePrefixFromFolderName(decodedFolderName); + return removePrefixFromFolderName(decodedFolderName) } - @Nullable - private String removePrefixFromFolderName(String folderName) { - String prefix = getCombinedPrefix(); - int prefixLength = prefix.length(); + private fun removePrefixFromFolderName(folderName: String): String? { + val prefix = getCombinedPrefix() + val prefixLength = prefix.length if (prefixLength == 0) { - return folderName; + return folderName } if (!folderName.startsWith(prefix)) { // Folder name doesn't start with our configured prefix. But right now when building commands we prefix all // folders except the INBOX with the prefix. So we won't be able to use this folder. - return null; + return null } - return folderName.substring(prefixLength); + return folderName.substring(prefixLength) } - public void checkSettings() throws MessagingException { + @Throws(MessagingException::class) + override fun checkSettings() { try { - ImapConnection connection = createImapConnection(); + val connection = createImapConnection() - connection.open(); - connection.close(); - } catch (IOException ioe) { - throw new MessagingException("Unable to connect", ioe); + connection.open() + connection.close() + } catch (e: IOException) { + throw MessagingException("Unable to connect", e) } } - @Override - @NotNull - public ImapConnection getConnection() throws MessagingException { - ImapConnection connection; - while ((connection = pollConnection()) != null) { + @Throws(MessagingException::class) + override fun getConnection(): ImapConnection { + while (true) { + val connection = pollConnection() ?: return createImapConnection() + try { - connection.executeSimpleCommand(Commands.NOOP); - break; - } catch (IOException ioe) { - connection.close(); - } - } + connection.executeSimpleCommand(Commands.NOOP) - if (connection == null) { - connection = createImapConnection(); + // If the command completes without an error this connection is still usable. + return connection + } catch (ioe: IOException) { + connection.close() + } } - - return connection; } - private ImapConnection pollConnection() { - synchronized (connections) { - return connections.poll(); + private fun pollConnection(): ImapConnection? { + return synchronized(connections) { + connections.poll() } } - @Override - public void releaseConnection(ImapConnection connection) { - if (connection != null && connection.isConnected()) { - if (connection.getConnectionGeneration() == connectionGeneration) { - synchronized (connections) { - connections.offer(connection); + override fun releaseConnection(connection: ImapConnection?) { + if (connection != null && connection.isConnected) { + if (connection.connectionGeneration == connectionGeneration) { + synchronized(connections) { + connections.offer(connection) } } else { - connection.close(); + connection.close() } } } - @Override - public void closeAllConnections() { - Timber.v("ImapStore.closeAllConnections()"); + override fun closeAllConnections() { + Timber.v("ImapStore.closeAllConnections()") + + val connectionsToClose = synchronized(connections) { + val connectionsToClose = connections.toList() - List connectionsToClose; - synchronized (connections) { - connectionGeneration++; - connectionsToClose = new ArrayList<>(connections); - connections.clear(); + connectionGeneration++ + connections.clear() + + connectionsToClose } - for (ImapConnection connection : connectionsToClose) { - connection.close(); + for (connection in connectionsToClose) { + connection.close() } } - ImapConnection createImapConnection() { - return new RealImapConnection( - new StoreImapSettings(), - trustedSocketFactory, - oauthTokenProvider, - connectionGeneration); + open fun createImapConnection(): ImapConnection { + return RealImapConnection( + StoreImapSettings(), + trustedSocketFactory, + oauthTokenProvider, + connectionGeneration + ) } - @Override - @NotNull - public String getLogLabel() { - return config.getLogLabel(); - } + override val logLabel: String + get() = config.logLabel - @Override - @NotNull - public Set getPermanentFlagsIndex() { - return permanentFlagsIndex; + override fun getPermanentFlagsIndex(): MutableSet { + return permanentFlagsIndex } - - private class StoreImapSettings implements ImapSettings { - @Override - public String getHost() { - return host; - } - - @Override - public int getPort() { - return port; - } - - @Override - public ConnectionSecurity getConnectionSecurity() { - return connectionSecurity; - } - - @Override - public AuthType getAuthType() { - return authType; - } - - @Override - public String getUsername() { - return username; - } - - @Override - public String getPassword() { - return password; - } - - @Override - public String getClientCertificateAlias() { - return clientCertificateAlias; - } - - @Override - public boolean useCompression() { - return config.useCompression(); - } - - @Override - public String getPathPrefix() { - return pathPrefix; + private inner class StoreImapSettings : ImapSettings { + override val host: String = this@RealImapStore.host + override val port: Int = serverSettings.port + override val connectionSecurity: ConnectionSecurity = serverSettings.connectionSecurity + override val authType: AuthType = serverSettings.authenticationType + override val username: String = serverSettings.username + override val password: String? = serverSettings.password + override val clientCertificateAlias: String? = serverSettings.clientCertificateAlias + + override fun useCompression(): Boolean { + return this@RealImapStore.config.useCompression() } - @Override - public void setPathPrefix(String prefix) { - pathPrefix = prefix; - } - - @Override - public String getPathDelimiter() { - return pathDelimiter; - } + override var pathPrefix: String? + get() = this@RealImapStore.pathPrefix + set(value) { + this@RealImapStore.pathPrefix = value + } - @Override - public void setPathDelimiter(String delimiter) { - pathDelimiter = delimiter; - } + override var pathDelimiter: String? + get() = this@RealImapStore.pathDelimiter + set(value) { + this@RealImapStore.pathDelimiter = value + } - @Override - public void setCombinedPrefix(String prefix) { - combinedPrefix = prefix; + override fun setCombinedPrefix(prefix: String?) { + combinedPrefix = prefix } } } + +private val ImapConnection.supportsListExtended: Boolean + get() = hasCapability(Capabilities.SPECIAL_USE) && hasCapability(Capabilities.LIST_EXTENDED) diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.java index ef95cf5810..e87a95b06e 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.java +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.java @@ -465,7 +465,7 @@ public class RealImapStoreTest { } @Override - ImapConnection createImapConnection() { + public ImapConnection createImapConnection() { if (imapConnections.isEmpty()) { throw new AssertionError("Unexpectedly tried to create an ImapConnection instance"); } -- GitLab From 48f13ca3dc765394b40debd1dcf6498a2a017247 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Aug 2022 17:41:36 +0200 Subject: [PATCH 26/85] Rename .java to .kt --- .../store/imap/{RealImapStoreTest.java => RealImapStoreTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/{RealImapStoreTest.java => RealImapStoreTest.kt} (100%) diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.kt similarity index 100% rename from mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.java rename to mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.kt -- GitLab From 002b70fcf9c99a8651dbfadd041dc91a767f0265 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Aug 2022 17:41:36 +0200 Subject: [PATCH 27/85] Convert `RealImapStoreTest` to Kotlin --- .../k9/mail/store/imap/RealImapStoreTest.kt | 684 ++++++++---------- 1 file changed, 318 insertions(+), 366 deletions(-) diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.kt index e87a95b06e..113b42f209 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.kt @@ -1,489 +1,441 @@ -package com.fsck.k9.mail.store.imap; - - -import java.io.IOException; -import java.util.ArrayDeque; -import java.util.Arrays; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.fsck.k9.mail.AuthType; -import com.fsck.k9.mail.ConnectionSecurity; -import com.fsck.k9.mail.FolderType; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.ServerSettings; -import com.fsck.k9.mail.oauth.OAuth2TokenProvider; -import com.fsck.k9.mail.ssl.TrustedSocketFactory; -import org.jetbrains.annotations.NotNull; -import org.junit.Before; -import org.junit.Test; -import org.mockito.internal.util.collections.Sets; - -import static com.fsck.k9.mail.store.imap.ImapResponseHelper.createImapResponse; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - - -public class RealImapStoreTest { - private ImapStoreConfig config = mock(ImapStoreConfig.class); - private TestImapStore imapStore; - - @Before - public void setUp() throws Exception { - ServerSettings serverSettings = createServerSettings(); - TrustedSocketFactory trustedSocketFactory = mock(TrustedSocketFactory.class); - OAuth2TokenProvider oauth2TokenProvider = mock(OAuth2TokenProvider.class); - - imapStore = new TestImapStore(serverSettings, config, trustedSocketFactory, - oauth2TokenProvider); - } +package com.fsck.k9.mail.store.imap + +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.ConnectionSecurity +import com.fsck.k9.mail.FolderType +import com.fsck.k9.mail.MessagingException +import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.ssl.TrustedSocketFactory +import com.fsck.k9.mail.store.imap.ImapResponseHelper.createImapResponse +import com.fsck.k9.mail.store.imap.ImapStoreSettings.createExtra +import com.google.common.truth.Truth.assertThat +import java.io.IOException +import java.util.ArrayDeque +import java.util.Deque +import org.junit.Assert.fail +import org.junit.Test +import org.mockito.ArgumentMatchers.anyString +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +class RealImapStoreTest { + private val imapStore = createTestImapStore() @Test - public void checkSettings_shouldCreateImapConnectionAndCallOpen() throws Exception { - ImapConnection imapConnection = createMockConnection(); - imapStore.enqueueImapConnection(imapConnection); + fun `checkSettings() should create ImapConnection and call open()`() { + val imapConnection = createMockConnection() + imapStore.enqueueImapConnection(imapConnection) - imapStore.checkSettings(); + imapStore.checkSettings() - verify(imapConnection).open(); + verify(imapConnection).open() } @Test - public void checkSettings_withOpenThrowing_shouldThrowMessagingException() throws Exception { - ImapConnection imapConnection = createMockConnection(); - doThrow(IOException.class).when(imapConnection).open(); - imapStore.enqueueImapConnection(imapConnection); + fun `checkSettings() with open throwing should throw MessagingException`() { + val imapConnection = createMockConnection().stub { + on { open() } doThrow IOException::class + } + imapStore.enqueueImapConnection(imapConnection) try { - imapStore.checkSettings(); - fail("Expected exception"); - } catch (MessagingException e) { - assertEquals("Unable to connect", e.getMessage()); - assertNotNull(e.getCause()); - assertEquals(IOException.class, e.getCause().getClass()); + imapStore.checkSettings() + fail("Expected exception") + } catch (e: MessagingException) { + assertThat(e).hasMessageThat().isEqualTo("Unable to connect") + assertThat(e).hasCauseThat().isInstanceOf(IOException::class.java) } } @Test - public void getFolders_withSpecialUseCapability_shouldReturnSpecialFolderInfo() throws Exception { - ImapConnection imapConnection = createMockConnection(); - when(imapConnection.hasCapability(Capabilities.LIST_EXTENDED)).thenReturn(true); - when(imapConnection.hasCapability(Capabilities.SPECIAL_USE)).thenReturn(true); - List imapResponses = Arrays.asList( - createImapResponse("* LIST (\\HasNoChildren) \"/\" \"INBOX\""), - createImapResponse("* LIST (\\Noselect \\HasChildren) \"/\" \"[Gmail]\""), - createImapResponse("* LIST (\\HasNoChildren \\All) \"/\" \"[Gmail]/All Mail\""), - createImapResponse("* LIST (\\HasNoChildren \\Drafts) \"/\" \"[Gmail]/Drafts\""), - createImapResponse("* LIST (\\HasNoChildren \\Important) \"/\" \"[Gmail]/Important\""), - createImapResponse("* LIST (\\HasNoChildren \\Sent) \"/\" \"[Gmail]/Sent Mail\""), - createImapResponse("* LIST (\\HasNoChildren \\Junk) \"/\" \"[Gmail]/Spam\""), - createImapResponse("* LIST (\\HasNoChildren \\Flagged) \"/\" \"[Gmail]/Starred\""), - createImapResponse("* LIST (\\HasNoChildren \\Trash) \"/\" \"[Gmail]/Trash\""), + fun `getFolders() with SPECIAL-USE capability should return special FolderInfo`() { + val imapConnection = createMockConnection().stub { + on { hasCapability(Capabilities.LIST_EXTENDED) } doReturn true + on { hasCapability(Capabilities.SPECIAL_USE) } doReturn true + on { executeSimpleCommand("""LIST "" "*" RETURN (SPECIAL-USE)""") } doReturn listOf( + createImapResponse("""* LIST (\HasNoChildren) "/" "INBOX""""), + createImapResponse("""* LIST (\Noselect \HasChildren) "/" "[Gmail]""""), + createImapResponse("""* LIST (\HasNoChildren \All) "/" "[Gmail]/All Mail""""), + createImapResponse("""* LIST (\HasNoChildren \Drafts) "/" "[Gmail]/Drafts""""), + createImapResponse("""* LIST (\HasNoChildren \Important) "/" "[Gmail]/Important""""), + createImapResponse("""* LIST (\HasNoChildren \Sent) "/" "[Gmail]/Sent Mail""""), + createImapResponse("""* LIST (\HasNoChildren \Junk) "/" "[Gmail]/Spam""""), + createImapResponse("""* LIST (\HasNoChildren \Flagged) "/" "[Gmail]/Starred""""), + createImapResponse("""* LIST (\HasNoChildren \Trash) "/" "[Gmail]/Trash""""), createImapResponse("5 OK Success") - ); - when(imapConnection.executeSimpleCommand("LIST \"\" \"*\" RETURN (SPECIAL-USE)")).thenReturn(imapResponses); - imapStore.enqueueImapConnection(imapConnection); - - List folders = imapStore.getFolders(); - - Map folderMap = toFolderMap(folders); - assertEquals(FolderType.INBOX, folderMap.get("INBOX").getType()); - assertEquals(FolderType.DRAFTS, folderMap.get("[Gmail]/Drafts").getType()); - assertEquals(FolderType.SENT, folderMap.get("[Gmail]/Sent Mail").getType()); - assertEquals(FolderType.SPAM, folderMap.get("[Gmail]/Spam").getType()); - assertEquals(FolderType.TRASH, folderMap.get("[Gmail]/Trash").getType()); - assertEquals(FolderType.ARCHIVE, folderMap.get("[Gmail]/All Mail").getType()); + ) + } + imapStore.enqueueImapConnection(imapConnection) + + val folders = imapStore.getFolders() + + val foldersMap = folders.map { it.serverId to it.type } + assertThat(foldersMap).containsExactly( + "INBOX" to FolderType.INBOX, + "[Gmail]/All Mail" to FolderType.ARCHIVE, + "[Gmail]/Drafts" to FolderType.DRAFTS, + "[Gmail]/Important" to FolderType.REGULAR, + "[Gmail]/Sent Mail" to FolderType.SENT, + "[Gmail]/Spam" to FolderType.SPAM, + "[Gmail]/Starred" to FolderType.REGULAR, + "[Gmail]/Trash" to FolderType.TRASH + ) } @Test - public void getFolders_withoutSpecialUseCapability_shouldUseSimpleListCommand() throws Exception { - ImapConnection imapConnection = createMockConnection(); - when(imapConnection.hasCapability(Capabilities.LIST_EXTENDED)).thenReturn(true); - when(imapConnection.hasCapability(Capabilities.SPECIAL_USE)).thenReturn(false); - imapStore.enqueueImapConnection(imapConnection); + fun `getFolders() without SPECIAL-USE capability should use simple LIST command`() { + val imapConnection = createMockConnection().stub { + on { hasCapability(Capabilities.LIST_EXTENDED) } doReturn true + on { hasCapability(Capabilities.SPECIAL_USE) } doReturn false + } + imapStore.enqueueImapConnection(imapConnection) - imapStore.getFolders(); + imapStore.getFolders() - verify(imapConnection, never()).executeSimpleCommand("LIST \"\" \"*\" RETURN (SPECIAL-USE)"); - verify(imapConnection).executeSimpleCommand("LIST \"\" \"*\""); + verify(imapConnection, never()).executeSimpleCommand("""LIST "" "*" RETURN (SPECIAL-USE)""") + verify(imapConnection).executeSimpleCommand("""LIST "" "*"""") } @Test - public void getFolders_withoutListExtendedCapability_shouldUseSimpleListCommand() throws Exception { - ImapConnection imapConnection = createMockConnection(); - when(imapConnection.hasCapability(Capabilities.LIST_EXTENDED)).thenReturn(false); - when(imapConnection.hasCapability(Capabilities.SPECIAL_USE)).thenReturn(true); - imapStore.enqueueImapConnection(imapConnection); + fun `getFolders() without LIST-EXTENDED capability should use simple LIST command`() { + val imapConnection = createMockConnection().stub { + on { hasCapability(Capabilities.LIST_EXTENDED) } doReturn false + on { hasCapability(Capabilities.SPECIAL_USE) } doReturn true + } + imapStore.enqueueImapConnection(imapConnection) - imapStore.getFolders(); + imapStore.getFolders() - verify(imapConnection, never()).executeSimpleCommand("LIST \"\" \"*\" RETURN (SPECIAL-USE)"); - verify(imapConnection).executeSimpleCommand("LIST \"\" \"*\""); + verify(imapConnection, never()).executeSimpleCommand("""LIST "" "*" RETURN (SPECIAL-USE)""") + verify(imapConnection).executeSimpleCommand("""LIST "" "*"""") } @Test - public void getFolders_withoutSubscribedFoldersOnly() throws Exception { - when(config.isSubscribedFoldersOnly()).thenReturn(false); - ImapConnection imapConnection = createMockConnection(); - List imapResponses = Arrays.asList( - createImapResponse("* LIST (\\HasNoChildren) \".\" \"INBOX\""), - createImapResponse("* LIST (\\Noselect \\HasChildren) \".\" \"Folder\""), - createImapResponse("* LIST (\\HasNoChildren) \".\" \"Folder.SubFolder\""), + fun `getFolders() with subscribedFoldersOnly = false`() { + val imapStore = createTestImapStore(isSubscribedFoldersOnly = false) + val imapConnection = createMockConnection().stub { + on { executeSimpleCommand("""LIST "" "*"""") } doReturn listOf( + createImapResponse("""* LIST (\HasNoChildren) "." "INBOX""""), + createImapResponse("""* LIST (\Noselect \HasChildren) "." "Folder""""), + createImapResponse("""* LIST (\HasNoChildren) "." "Folder.SubFolder""""), createImapResponse("6 OK Success") - ); - when(imapConnection.executeSimpleCommand("LIST \"\" \"*\"")).thenReturn(imapResponses); - imapStore.enqueueImapConnection(imapConnection); + ) + } + imapStore.enqueueImapConnection(imapConnection) - List result = imapStore.getFolders(); + val folders = imapStore.getFolders() - assertNotNull(result); - assertEquals(Sets.newSet("INBOX", "Folder.SubFolder"), extractFolderServerIds(result)); + assertThat(folders).isNotNull() + assertThat(folders.map { it.serverId }).containsExactly("INBOX", "Folder.SubFolder") } @Test - public void getFolders_withSubscribedFoldersOnly_shouldOnlyReturnExistingSubscribedFolders() - throws Exception { - when(config.isSubscribedFoldersOnly()).thenReturn(true); - ImapConnection imapConnection = createMockConnection(); - List lsubResponses = Arrays.asList( - createImapResponse("* LSUB (\\HasNoChildren) \".\" \"INBOX\""), - createImapResponse("* LSUB (\\Noselect \\HasChildren) \".\" \"Folder\""), - createImapResponse("* LSUB (\\HasNoChildren) \".\" \"Folder.SubFolder\""), - createImapResponse("* LSUB (\\HasNoChildren) \".\" \"SubscribedFolderThatHasBeenDeleted\""), + fun `getFolders() with subscribedFoldersOnly = true should only return existing subscribed folders`() { + val imapStore = createTestImapStore(isSubscribedFoldersOnly = true) + val imapConnection = createMockConnection().stub { + on { executeSimpleCommand("""LSUB "" "*"""") } doReturn listOf( + createImapResponse("""* LSUB (\HasNoChildren) "." "INBOX""""), + createImapResponse("""* LSUB (\Noselect \HasChildren) "." "Folder""""), + createImapResponse("""* LSUB (\HasNoChildren) "." "Folder.SubFolder""""), + createImapResponse("""* LSUB (\HasNoChildren) "." "SubscribedFolderThatHasBeenDeleted""""), createImapResponse("5 OK Success") - ); - when(imapConnection.executeSimpleCommand("LSUB \"\" \"*\"")).thenReturn(lsubResponses); - List imapResponses = Arrays.asList( - createImapResponse("* LIST (\\HasNoChildren) \".\" \"INBOX\""), - createImapResponse("* LIST (\\Noselect \\HasChildren) \".\" \"Folder\""), - createImapResponse("* LIST (\\HasNoChildren) \".\" \"Folder.SubFolder\""), + ) + on { executeSimpleCommand("""LIST "" "*"""") } doReturn listOf( + createImapResponse("""* LIST (\HasNoChildren) "." "INBOX""""), + createImapResponse("""* LIST (\Noselect \HasChildren) "." "Folder""""), + createImapResponse("""* LIST (\HasNoChildren) "." "Folder.SubFolder""""), createImapResponse("6 OK Success") - ); - when(imapConnection.executeSimpleCommand("LIST \"\" \"*\"")).thenReturn(imapResponses); - imapStore.enqueueImapConnection(imapConnection); + ) + } + imapStore.enqueueImapConnection(imapConnection) - List result = imapStore.getFolders(); + val folders = imapStore.getFolders() - assertNotNull(result); - assertEquals(Sets.newSet("INBOX", "Folder.SubFolder"), extractFolderServerIds(result)); + assertThat(folders).isNotNull() + assertThat(folders.map { it.serverId }).containsExactly("INBOX", "Folder.SubFolder") } @Test - public void getFolders_withNamespacePrefix() throws Exception { - ImapConnection imapConnection = createMockConnection(); - List imapResponses = Arrays.asList( - createImapResponse("* LIST () \".\" \"INBOX\""), - createImapResponse("* LIST () \".\" \"INBOX.FolderOne\""), - createImapResponse("* LIST () \".\" \"INBOX.FolderTwo\""), + fun `getFolders() with namespace prefix`() { + val imapConnection = createMockConnection().stub { + on { executeSimpleCommand("""LIST "" "INBOX.*"""") } doReturn listOf( + createImapResponse("""* LIST () "." "INBOX""""), + createImapResponse("""* LIST () "." "INBOX.FolderOne""""), + createImapResponse("""* LIST () "." "INBOX.FolderTwo""""), createImapResponse("5 OK Success") - ); - when(imapConnection.executeSimpleCommand("LIST \"\" \"INBOX.*\"")).thenReturn(imapResponses); - imapStore.enqueueImapConnection(imapConnection); - imapStore.setTestCombinedPrefix("INBOX."); + ) + } + imapStore.enqueueImapConnection(imapConnection) + imapStore.setTestCombinedPrefix("INBOX.") - List result = imapStore.getFolders(); + val folders = imapStore.getFolders() - assertNotNull(result); - assertEquals(Sets.newSet("INBOX", "INBOX.FolderOne", "INBOX.FolderTwo"), extractFolderServerIds(result)); - assertEquals(Sets.newSet("INBOX", "FolderOne", "FolderTwo"), extractFolderNames(result)); - assertEquals(Sets.newSet("INBOX", "FolderOne", "FolderTwo"), extractOldFolderServerIds(result)); + assertThat(folders).isNotNull() + assertThat(folders.map { it.serverId }).containsExactly("INBOX", "INBOX.FolderOne", "INBOX.FolderTwo") + assertThat(folders.map { it.name }).containsExactly("INBOX", "FolderOne", "FolderTwo") + assertThat(folders.map { it.oldServerId }).containsExactly("INBOX", "FolderOne", "FolderTwo") } @Test - public void getFolders_withFolderNotMatchingNamespacePrefix() throws Exception { - ImapConnection imapConnection = createMockConnection(); - List imapResponses = Arrays.asList( - createImapResponse("* LIST () \".\" \"INBOX\""), - createImapResponse("* LIST () \".\" \"INBOX.FolderOne\""), - createImapResponse("* LIST () \".\" \"FolderTwo\""), + fun `getFolders() with folder not matching namespace prefix`() { + val imapConnection = createMockConnection().stub { + on { executeSimpleCommand("""LIST "" "INBOX.*"""") } doReturn listOf( + createImapResponse("""* LIST () "." "INBOX""""), + createImapResponse("""* LIST () "." "INBOX.FolderOne""""), + createImapResponse("""* LIST () "." "FolderTwo""""), createImapResponse("5 OK Success") - ); - when(imapConnection.executeSimpleCommand("LIST \"\" \"INBOX.*\"")).thenReturn(imapResponses); - imapStore.enqueueImapConnection(imapConnection); - imapStore.setTestCombinedPrefix("INBOX."); + ) + } + imapStore.enqueueImapConnection(imapConnection) + imapStore.setTestCombinedPrefix("INBOX.") - List result = imapStore.getFolders(); + val folders = imapStore.getFolders() - assertNotNull(result); - assertEquals(Sets.newSet("INBOX", "INBOX.FolderOne", "FolderTwo"), extractFolderServerIds(result)); - assertEquals(Sets.newSet("INBOX", "FolderOne", "FolderTwo"), extractFolderNames(result)); - assertEquals(Sets.newSet("INBOX", "FolderOne"), extractOldFolderServerIds(result)); + assertThat(folders).isNotNull() + assertThat(folders.map { it.serverId }).containsExactly("INBOX", "INBOX.FolderOne", "FolderTwo") + assertThat(folders.map { it.name }).containsExactly("INBOX", "FolderOne", "FolderTwo") + assertThat(folders.mapNotNull { it.oldServerId }).containsExactly("INBOX", "FolderOne") } @Test - public void getFolders_withDuplicateFolderNames_shouldRemoveDuplicatesAndKeepFolderType() - throws Exception { - ImapConnection imapConnection = createMockConnection(); - when(imapConnection.hasCapability(Capabilities.LIST_EXTENDED)).thenReturn(true); - when(imapConnection.hasCapability(Capabilities.SPECIAL_USE)).thenReturn(true); - List imapResponses = Arrays.asList( - createImapResponse("* LIST () \".\" \"INBOX\""), - createImapResponse("* LIST (\\HasNoChildren) \".\" \"Junk\""), - createImapResponse("* LIST (\\Junk) \".\" \"Junk\""), - createImapResponse("* LIST (\\HasNoChildren) \".\" \"Junk\""), + fun `getFolders() with duplicate folder names should remove duplicates and keep FolderType`() { + val imapConnection = createMockConnection().stub { + on { hasCapability(Capabilities.LIST_EXTENDED) } doReturn true + on { hasCapability(Capabilities.SPECIAL_USE) } doReturn true + on { executeSimpleCommand("""LIST "" "*" RETURN (SPECIAL-USE)""") } doReturn listOf( + createImapResponse("""* LIST () "." "INBOX""""), + createImapResponse("""* LIST (\HasNoChildren) "." "Junk""""), + createImapResponse("""* LIST (\Junk) "." "Junk""""), + createImapResponse("""* LIST (\HasNoChildren) "." "Junk""""), createImapResponse("5 OK Success") - ); - when(imapConnection.executeSimpleCommand("LIST \"\" \"*\" RETURN (SPECIAL-USE)")).thenReturn(imapResponses); - imapStore.enqueueImapConnection(imapConnection); + ) + } + imapStore.enqueueImapConnection(imapConnection) - List result = imapStore.getFolders(); + val folders = imapStore.getFolders() - assertNotNull(result); - assertEquals(2, result.size()); - FolderListItem junkFolder = getFolderByServerId(result, "Junk"); - assertNotNull(junkFolder); - assertEquals(FolderType.SPAM, junkFolder.getType()); + assertThat(folders.map { it.serverId to it.type }).containsExactly( + "INBOX" to FolderType.INBOX, + "Junk" to FolderType.SPAM + ) } @Test - public void getFolders_withoutException_shouldLeaveImapConnectionOpen() throws Exception { - ImapConnection imapConnection = createMockConnection(); - List imapResponses = Collections.singletonList(createImapResponse("5 OK Success")); - when(imapConnection.executeSimpleCommand(anyString())).thenReturn(imapResponses); - imapStore.enqueueImapConnection(imapConnection); + fun `getFolders() without exception should leave ImapConnection open`() { + val imapConnection = createMockConnection().stub { + on { executeSimpleCommand(anyString()) } doReturn listOf(createImapResponse("5 OK Success")) + } + imapStore.enqueueImapConnection(imapConnection) - imapStore.getFolders(); + imapStore.getFolders() - verify(imapConnection, never()).close(); + verify(imapConnection, never()).close() } @Test - public void getFolders_withIoException_shouldCloseImapConnection() throws Exception { - ImapConnection imapConnection = createMockConnection(); - doThrow(IOException.class).when(imapConnection).executeSimpleCommand("LIST \"\" \"*\""); - imapStore.enqueueImapConnection(imapConnection); + fun `getFolders() with IOException should close ImapConnection`() { + val imapConnection = createMockConnection().stub { + on { executeSimpleCommand("""LIST "" "*"""") } doThrow IOException::class + } + imapStore.enqueueImapConnection(imapConnection) try { - imapStore.getFolders(); - fail("Expected exception"); - } catch (MessagingException ignored) { + imapStore.getFolders() + fail("Expected exception") + } catch (ignored: MessagingException) { } - verify(imapConnection).close(); + verify(imapConnection).close() } @Test - public void getConnection_shouldCreateImapConnection() throws Exception { - ImapConnection imapConnection = createMockConnection(); - imapStore.enqueueImapConnection(imapConnection); + fun `getConnection() should create ImapConnection`() { + val imapConnection = createMockConnection() + imapStore.enqueueImapConnection(imapConnection) - ImapConnection result = imapStore.getConnection(); + val result = imapStore.getConnection() - assertSame(imapConnection, result); + assertThat(result).isSameInstanceAs(imapConnection) } @Test - public void getConnection_calledTwiceWithoutRelease_shouldCreateTwoImapConnection() throws Exception { - ImapConnection imapConnectionOne = createMockConnection(); - ImapConnection imapConnectionTwo = createMockConnection(); - imapStore.enqueueImapConnection(imapConnectionOne); - imapStore.enqueueImapConnection(imapConnectionTwo); + fun `getConnection() called twice without release should create two ImapConnection instances`() { + val imapConnectionOne = createMockConnection() + val imapConnectionTwo = createMockConnection() + imapStore.enqueueImapConnection(imapConnectionOne) + imapStore.enqueueImapConnection(imapConnectionTwo) - ImapConnection resultOne = imapStore.getConnection(); - ImapConnection resultTwo = imapStore.getConnection(); + val resultOne = imapStore.getConnection() + val resultTwo = imapStore.getConnection() - assertSame(imapConnectionOne, resultOne); - assertSame(imapConnectionTwo, resultTwo); + assertThat(resultOne).isSameInstanceAs(imapConnectionOne) + assertThat(resultTwo).isSameInstanceAs(imapConnectionTwo) } @Test - public void getConnection_calledAfterRelease_shouldReturnCachedImapConnection() throws Exception { - ImapConnection imapConnection = createMockConnection(); - when(imapConnection.isConnected()).thenReturn(true); - imapStore.enqueueImapConnection(imapConnection); - ImapConnection connection = imapStore.getConnection(); - imapStore.releaseConnection(connection); + fun `getConnection() called after release should return cached ImapConnection`() { + val imapConnection = createMockConnection().stub { + on { isConnected } doReturn true + } + imapStore.enqueueImapConnection(imapConnection) - ImapConnection result = imapStore.getConnection(); + val connection = imapStore.getConnection() + imapStore.releaseConnection(connection) - assertSame(imapConnection, result); - } + val result = imapStore.getConnection() - @Test - public void getConnection_calledAfterReleaseWithAClosedConnection_shouldReturnNewImapConnectionInstance() - throws Exception { - ImapConnection imapConnectionOne = createMockConnection(); - ImapConnection imapConnectionTwo = createMockConnection(); - imapStore.enqueueImapConnection(imapConnectionOne); - imapStore.enqueueImapConnection(imapConnectionTwo); - imapStore.getConnection(); - when(imapConnectionOne.isConnected()).thenReturn(false); - imapStore.releaseConnection(imapConnectionOne); - - ImapConnection result = imapStore.getConnection(); - - assertSame(imapConnectionTwo, result); + assertThat(result).isSameInstanceAs(imapConnection) } @Test - public void getConnection_withDeadConnectionInPool_shouldReturnNewImapConnectionInstance() throws Exception { - ImapConnection imapConnectionOne = createMockConnection(); - ImapConnection imapConnectionTwo = createMockConnection(); - imapStore.enqueueImapConnection(imapConnectionOne); - imapStore.enqueueImapConnection(imapConnectionTwo); - imapStore.getConnection(); - when(imapConnectionOne.isConnected()).thenReturn(true); - doThrow(IOException.class).when(imapConnectionOne).executeSimpleCommand(Commands.NOOP); - imapStore.releaseConnection(imapConnectionOne); - - ImapConnection result = imapStore.getConnection(); - - assertSame(imapConnectionTwo, result); - } + fun `getConnection() called after release with closed connection should return new ImapConnection instance`() { + val imapConnectionOne = createMockConnection() + val imapConnectionTwo = createMockConnection() + imapStore.enqueueImapConnection(imapConnectionOne) + imapStore.enqueueImapConnection(imapConnectionTwo) + + imapStore.getConnection() + imapConnectionOne.stub { + on { isConnected } doReturn false + } + imapStore.releaseConnection(imapConnectionOne) - @Test - public void getConnection_withConnectionInPoolAndCloseAllConnections_shouldReturnNewImapConnectionInstance() - throws Exception { - ImapConnection imapConnectionOne = createMockConnection(1); - ImapConnection imapConnectionTwo = createMockConnection(2); - imapStore.enqueueImapConnection(imapConnectionOne); - imapStore.enqueueImapConnection(imapConnectionTwo); - imapStore.getConnection(); - when(imapConnectionOne.isConnected()).thenReturn(true); - imapStore.releaseConnection(imapConnectionOne); - imapStore.closeAllConnections(); - - ImapConnection result = imapStore.getConnection(); - - assertSame(imapConnectionTwo, result); + val result = imapStore.getConnection() + + assertThat(result).isSameInstanceAs(imapConnectionTwo) } @Test - public void getConnection_withConnectionOutsideOfPoolAndCloseAllConnections_shouldReturnNewImapConnectionInstance() - throws Exception { - ImapConnection imapConnectionOne = createMockConnection(1); - ImapConnection imapConnectionTwo = createMockConnection(2); - imapStore.enqueueImapConnection(imapConnectionOne); - imapStore.enqueueImapConnection(imapConnectionTwo); - imapStore.getConnection(); - when(imapConnectionOne.isConnected()).thenReturn(true); - imapStore.closeAllConnections(); - imapStore.releaseConnection(imapConnectionOne); - - ImapConnection result = imapStore.getConnection(); - - assertSame(imapConnectionTwo, result); - } + fun `getConnection() with dead connection in pool should return new ImapConnection instance`() { + val imapConnectionOne = createMockConnection() + val imapConnectionTwo = createMockConnection() + imapStore.enqueueImapConnection(imapConnectionOne) + imapStore.enqueueImapConnection(imapConnectionTwo) + + imapStore.getConnection() + imapConnectionOne.stub { + on { isConnected } doReturn true + on { executeSimpleCommand(Commands.NOOP) } doThrow IOException::class + } + imapStore.releaseConnection(imapConnectionOne) + val result = imapStore.getConnection() - private ImapConnection createMockConnection() { - ImapConnection imapConnection = mock(ImapConnection.class); - when(imapConnection.getConnectionGeneration()).thenReturn(1); - return imapConnection; + assertThat(result).isSameInstanceAs(imapConnectionTwo) } - private ImapConnection createMockConnection(int connectionGeneration) { - ImapConnection imapConnection = mock(ImapConnection.class); - when(imapConnection.getConnectionGeneration()).thenReturn(connectionGeneration); - return imapConnection; - } + @Test + fun `getConnection() with connection in pool and closeAllConnections() should return new ImapConnection instance`() { + val imapConnectionOne = createMockConnection(1) + val imapConnectionTwo = createMockConnection(2) + imapStore.enqueueImapConnection(imapConnectionOne) + imapStore.enqueueImapConnection(imapConnectionTwo) + + imapStore.getConnection() + imapConnectionOne.stub { + on { isConnected } doReturn true + } + imapStore.releaseConnection(imapConnectionOne) + imapStore.closeAllConnections() + val result = imapStore.getConnection() - private ServerSettings createServerSettings() { - Map extra = ImapStoreSettings.createExtra(true, null); - return new ServerSettings( - "imap", - "imap.example.org", - 143, - ConnectionSecurity.NONE, - AuthType.PLAIN, - "user", - "password", - null, - extra); + assertThat(result).isSameInstanceAs(imapConnectionTwo) } - private Set extractFolderServerIds(List folders) { - Set folderServerIds = new HashSet<>(folders.size()); - for (FolderListItem folder : folders) { - folderServerIds.add(folder.getServerId()); + @Test + fun `getConnection() with connection outside of pool and closeAllConnections() should return new ImapConnection instance`() { + val imapConnectionOne = createMockConnection(1) + val imapConnectionTwo = createMockConnection(2) + imapStore.enqueueImapConnection(imapConnectionOne) + imapStore.enqueueImapConnection(imapConnectionTwo) + + imapStore.getConnection() + imapConnectionOne.stub { + on { isConnected } doReturn true } + imapStore.closeAllConnections() + imapStore.releaseConnection(imapConnectionOne) - return folderServerIds; - } + val result = imapStore.getConnection() - private Set extractFolderNames(List folders) { - Set folderNames = new HashSet<>(folders.size()); - for (FolderListItem folder : folders) { - folderNames.add(folder.getName()); - } - - return folderNames; + assertThat(result).isSameInstanceAs(imapConnectionTwo) } - private Set extractOldFolderServerIds(List folders) { - Set folderNames = new HashSet<>(folders.size()); - for (FolderListItem folder : folders) { - String oldServerId = folder.getOldServerId(); - if (oldServerId != null) { - folderNames.add(oldServerId); - } + private fun createMockConnection(connectionGeneration: Int = 1): ImapConnection { + return mock { + on { this.connectionGeneration } doReturn connectionGeneration } - - return folderNames; } - private FolderListItem getFolderByServerId(List result, String serverId) { - for (FolderListItem imapFolder : result) { - if (imapFolder.getServerId().equals(serverId)) { - return imapFolder; - } - } - return null; + private fun createServerSettings(): ServerSettings { + return ServerSettings( + type = "imap", + host = "imap.example.org", + port = 143, + connectionSecurity = ConnectionSecurity.NONE, + authenticationType = AuthType.PLAIN, + username = "user", + password = "password", + clientCertificateAlias = null, + extra = createExtra(autoDetectNamespace = true, pathPrefix = null) + ) } - private Map toFolderMap(List folders) { - Map folderMap = new HashMap<>(); - for (FolderListItem folder : folders) { - folderMap.put(folder.getServerId(), folder); - } - - return folderMap; + private fun createTestImapStore( + isSubscribedFoldersOnly: Boolean = false, + useCompression: Boolean = false + ): TestImapStore { + return TestImapStore( + serverSettings = createServerSettings(), + config = createImapStoreConfig(isSubscribedFoldersOnly, useCompression), + trustedSocketFactory = mock(), + oauth2TokenProvider = null + ) } - - static class TestImapStore extends RealImapStore { - private Deque imapConnections = new ArrayDeque<>(); - private String testCombinedPrefix; - - public TestImapStore(ServerSettings serverSettings, ImapStoreConfig config, - TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oauth2TokenProvider) { - super(serverSettings, config, trustedSocketFactory, oauth2TokenProvider); + private fun createImapStoreConfig(isSubscribedFoldersOnly: Boolean, useCompression: Boolean): ImapStoreConfig { + return object : ImapStoreConfig { + override val logLabel: String = "irrelevant" + override fun isSubscribedFoldersOnly(): Boolean = isSubscribedFoldersOnly + override fun useCompression(): Boolean = useCompression } + } - @Override - public ImapConnection createImapConnection() { + private class TestImapStore( + serverSettings: ServerSettings, + config: ImapStoreConfig, + trustedSocketFactory: TrustedSocketFactory, + oauth2TokenProvider: OAuth2TokenProvider? + ) : RealImapStore( + serverSettings, config, trustedSocketFactory, oauth2TokenProvider + ) { + private val imapConnections: Deque = ArrayDeque() + private var testCombinedPrefix: String? = null + + override fun createImapConnection(): ImapConnection { if (imapConnections.isEmpty()) { - throw new AssertionError("Unexpectedly tried to create an ImapConnection instance"); + throw AssertionError("Unexpectedly tried to create an ImapConnection instance") } - return imapConnections.pop(); + + return imapConnections.pop() } - public void enqueueImapConnection(ImapConnection imapConnection) { - imapConnections.add(imapConnection); + fun enqueueImapConnection(imapConnection: ImapConnection) { + imapConnections.add(imapConnection) } - @Override - @NotNull - public String getCombinedPrefix() { - return testCombinedPrefix != null ? testCombinedPrefix : super.getCombinedPrefix(); + override fun getCombinedPrefix(): String { + return testCombinedPrefix ?: super.getCombinedPrefix() } - void setTestCombinedPrefix(String prefix) { - testCombinedPrefix = prefix; + fun setTestCombinedPrefix(prefix: String?) { + testCombinedPrefix = prefix } } } -- GitLab From 8334e692ad6ac5ed459474ee82ec5c1ec84dfafb Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Aug 2022 14:33:41 +0200 Subject: [PATCH 28/85] Rename .java to .kt --- .../store/imap/{RealImapConnection.java => RealImapConnection.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/{RealImapConnection.java => RealImapConnection.kt} (100%) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt similarity index 100% rename from mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java rename to mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt -- GitLab From 5bd3b1a7f7f8c0f2c27271febed6a5842ef1c941 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Aug 2022 14:33:41 +0200 Subject: [PATCH 29/85] Convert `RealImapConnection` to Kotlin --- .../k9/mail/store/imap/RealImapConnection.kt | 1155 ++++++++--------- .../mail/store/imap/RealImapConnectionTest.kt | 4 +- .../k9/mail/store/imap/RealImapFolderTest.kt | 4 +- 3 files changed, 566 insertions(+), 597 deletions(-) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt index 6f927b06f8..8798a230d7 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt @@ -1,325 +1,279 @@ -package com.fsck.k9.mail.store.imap; - - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.ConnectException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; -import java.security.GeneralSecurityException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.Security; -import java.security.cert.CertificateException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; - -import com.fsck.k9.logging.Timber; -import com.fsck.k9.mail.Authentication; -import com.fsck.k9.mail.AuthenticationFailedException; -import com.fsck.k9.mail.CertificateValidationException; -import com.fsck.k9.mail.ConnectionSecurity; -import com.fsck.k9.mail.K9MailLib; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.filter.Base64; -import com.fsck.k9.mail.filter.PeekableInputStream; -import com.fsck.k9.mail.oauth.OAuth2TokenProvider; -import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser; -import com.fsck.k9.mail.ssl.TrustedSocketFactory; -import com.fsck.k9.mail.store.imap.IdGrouper.GroupedIds; -import com.fsck.k9.sasl.OAuthBearer; -import com.jcraft.jzlib.JZlib; -import com.jcraft.jzlib.ZOutputStream; -import javax.net.ssl.SSLException; -import org.apache.commons.io.IOUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import static com.fsck.k9.mail.ConnectionSecurity.STARTTLS_REQUIRED; -import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_IMAP; -import static com.fsck.k9.mail.NetworkTimeouts.SOCKET_CONNECT_TIMEOUT; -import static com.fsck.k9.mail.NetworkTimeouts.SOCKET_READ_TIMEOUT; -import static com.fsck.k9.mail.store.imap.ImapResponseParser.equalsIgnoreCase; - +package com.fsck.k9.mail.store.imap + +import com.fsck.k9.logging.Timber +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.Authentication +import com.fsck.k9.mail.AuthenticationFailedException +import com.fsck.k9.mail.CertificateValidationException +import com.fsck.k9.mail.ConnectionSecurity +import com.fsck.k9.mail.K9MailLib +import com.fsck.k9.mail.MessagingException +import com.fsck.k9.mail.NetworkTimeouts.SOCKET_CONNECT_TIMEOUT +import com.fsck.k9.mail.NetworkTimeouts.SOCKET_READ_TIMEOUT +import com.fsck.k9.mail.filter.Base64 +import com.fsck.k9.mail.filter.PeekableInputStream +import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser +import com.fsck.k9.mail.ssl.TrustedSocketFactory +import com.fsck.k9.sasl.buildOAuthBearerInitialClientResponse +import com.jcraft.jzlib.JZlib +import com.jcraft.jzlib.ZOutputStream +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.ConnectException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Socket +import java.net.SocketAddress +import java.security.GeneralSecurityException +import java.security.Security +import java.security.cert.CertificateException +import java.util.regex.Pattern +import java.util.zip.Inflater +import java.util.zip.InflaterInputStream +import javax.net.ssl.SSLException +import org.apache.commons.io.IOUtils /** * A cacheable class that stores the details for a single IMAP connection. */ -class RealImapConnection implements ImapConnection { - private static final int BUFFER_SIZE = 1024; - - /* The below limits are 20 octets less than the recommended limits, in order to compensate for - * the length of the command tag, the space after the tag and the CRLF at the end of the command - * (these are not taken into account when calculating the length of the command). For more - * information, refer to section 4 of RFC 7162. - * - * The length limit for servers supporting the CONDSTORE extension is large in order to support - * the QRESYNC parameter to the SELECT/EXAMINE commands, which accept a list of known message - * sequence numbers as well as their corresponding UIDs. - */ - private static final int LENGTH_LIMIT_WITHOUT_CONDSTORE = 980; - private static final int LENGTH_LIMIT_WITH_CONDSTORE = 8172; - - - private final OAuth2TokenProvider oauthTokenProvider; - private final TrustedSocketFactory socketFactory; - private final int socketConnectTimeout; - private final int socketReadTimeout; - private final int connectionGeneration; - - private Socket socket; - private PeekableInputStream inputStream; - private OutputStream outputStream; - private ImapResponseParser responseParser; - private int nextCommandTag; - private Set capabilities = new HashSet<>(); - private ImapSettings settings; - private Exception stacktraceForClose; - private boolean open = false; - private boolean retryOAuthWithNewToken = true; - - - public RealImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory, - OAuth2TokenProvider oauthTokenProvider, int connectionGeneration) { - this.settings = settings; - this.socketFactory = socketFactory; - this.oauthTokenProvider = oauthTokenProvider; - this.socketConnectTimeout = SOCKET_CONNECT_TIMEOUT; - this.socketReadTimeout = SOCKET_READ_TIMEOUT; - this.connectionGeneration = connectionGeneration; - } - - public RealImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory, - OAuth2TokenProvider oauthTokenProvider, int socketConnectTimeout, int socketReadTimeout, - int connectionGeneration) { - this.settings = settings; - this.socketFactory = socketFactory; - this.oauthTokenProvider = oauthTokenProvider; - this.socketConnectTimeout = socketConnectTimeout; - this.socketReadTimeout = socketReadTimeout; - this.connectionGeneration = connectionGeneration; - } - - @Override - public synchronized void open() throws IOException, MessagingException { +internal class RealImapConnection( + private val settings: ImapSettings, + private val socketFactory: TrustedSocketFactory, + private val oauthTokenProvider: OAuth2TokenProvider?, + override val connectionGeneration: Int, + private val socketConnectTimeout: Int = SOCKET_CONNECT_TIMEOUT, + private val socketReadTimeout: Int = SOCKET_READ_TIMEOUT +) : ImapConnection { + private var socket: Socket? = null + private var inputStream: PeekableInputStream? = null + private var imapOutputStream: OutputStream? = null + private var responseParser: ImapResponseParser? = null + private var nextCommandTag = 0 + private var capabilities = emptySet() + private var stacktraceForClose: Exception? = null + private var open = false + private var retryOAuthWithNewToken = true + + @get:Synchronized + override val outputStream: OutputStream + get() = checkNotNull(imapOutputStream) + + @Synchronized + @Throws(IOException::class, MessagingException::class) + override fun open() { if (open) { - return; + return } else if (stacktraceForClose != null) { - throw new IllegalStateException("open() called after close(). " + - "Check wrapped exception to see where close() was called.", stacktraceForClose); + throw IllegalStateException( + "open() called after close(). Check wrapped exception to see where close() was called.", + stacktraceForClose + ) } - open = true; - boolean authSuccess = false; - nextCommandTag = 1; + open = true + var authSuccess = false + nextCommandTag = 1 - adjustDNSCacheTTL(); + adjustDNSCacheTTL() try { - socket = connect(); - configureSocket(); - setUpStreamsAndParserFromSocket(); - - readInitialResponse(); - requestCapabilitiesIfNecessary(); + socket = connect() + configureSocket() + setUpStreamsAndParserFromSocket() - upgradeToTlsIfNecessary(); + readInitialResponse() + requestCapabilitiesIfNecessary() - List responses = authenticate(); - authSuccess = true; + upgradeToTlsIfNecessary() - extractOrRequestCapabilities(responses); + val responses = authenticate() + authSuccess = true - enableCompressionIfRequested(); + extractOrRequestCapabilities(responses) - retrievePathPrefixIfNecessary(); - retrievePathDelimiterIfNecessary(); + enableCompressionIfRequested() - } catch (SSLException e) { - handleSslException(e); - } catch (ConnectException e) { - handleConnectException(e); - } catch (GeneralSecurityException e) { - throw new MessagingException("Unable to open connection to IMAP server due to security error.", e); + retrievePathPrefixIfNecessary() + retrievePathDelimiterIfNecessary() + } catch (e: SSLException) { + handleSslException(e) + } catch (e: ConnectException) { + handleConnectException(e) + } catch (e: GeneralSecurityException) { + throw MessagingException("Unable to open connection to IMAP server due to security error.", e) } finally { if (!authSuccess) { - Timber.e("Failed to login, closing connection for %s", getLogId()); - close(); + Timber.e("Failed to login, closing connection for %s", logId) + close() } } } - private void handleSslException(SSLException e) throws CertificateValidationException, SSLException { - if (e.getCause() instanceof CertificateException) { - throw new CertificateValidationException(e.getMessage(), e); + private fun handleSslException(e: SSLException) { + if (e.cause is CertificateException) { + throw CertificateValidationException(e.message, e) } else { - throw e; + throw e } } - private void handleConnectException(ConnectException e) throws ConnectException { - String message = e.getMessage(); - String[] tokens = message.split("-"); + // TODO: Remove this. There is no documentation on why this was added, there are no tests, and this is unlikely to + // still work. + private fun handleConnectException(e: ConnectException) { + val message = e.message ?: throw e - if (tokens.length > 1 && tokens[1] != null) { - Timber.e(e, "Stripping host/port from ConnectionException for %s", getLogId()); - throw new ConnectException(tokens[1].trim()); + val tokens = message.split("-") + if (tokens.size > 1) { + Timber.e(e, "Stripping host/port from ConnectionException for %s", logId) + throw ConnectException(tokens[1].trim()) } else { - throw e; + throw e } } - @Override - public synchronized boolean isConnected() { - return inputStream != null && outputStream != null && socket != null && - socket.isConnected() && !socket.isClosed(); - } + @get:Synchronized + override val isConnected: Boolean + get() { + return inputStream != null && imapOutputStream != null && + socket.let { socket -> + socket != null && socket.isConnected && !socket.isClosed + } + } - private void adjustDNSCacheTTL() { + private fun adjustDNSCacheTTL() { try { - Security.setProperty("networkaddress.cache.ttl", "0"); - } catch (Exception e) { - Timber.w(e, "Could not set DNS ttl to 0 for %s", getLogId()); + Security.setProperty("networkaddress.cache.ttl", "0") + } catch (e: Exception) { + Timber.w(e, "Could not set DNS ttl to 0 for %s", logId) } try { - Security.setProperty("networkaddress.cache.negative.ttl", "0"); - } catch (Exception e) { - Timber.w(e, "Could not set DNS negative ttl to 0 for %s", getLogId()); + Security.setProperty("networkaddress.cache.negative.ttl", "0") + } catch (e: Exception) { + Timber.w(e, "Could not set DNS negative ttl to 0 for %s", logId) } } - private Socket connect() throws GeneralSecurityException, MessagingException, IOException { - Exception connectException = null; + private fun connect(): Socket { + val inetAddresses = InetAddress.getAllByName(settings.host) - InetAddress[] inetAddresses = InetAddress.getAllByName(settings.getHost()); - for (InetAddress address : inetAddresses) { - try { - return connectToAddress(address); - } catch (IOException e) { - Timber.w(e, "Could not connect to %s", address); - connectException = e; + var connectException: Exception? = null + for (address in inetAddresses) { + connectException = try { + return connectToAddress(address) + } catch (e: IOException) { + Timber.w(e, "Could not connect to %s", address) + e } } - throw new MessagingException("Cannot connect to host", connectException); + throw MessagingException("Cannot connect to host", connectException) } - private Socket connectToAddress(InetAddress address) throws NoSuchAlgorithmException, KeyManagementException, - MessagingException, IOException { + private fun connectToAddress(address: InetAddress): Socket { + val host = settings.host + val port = settings.port + val clientCertificateAlias = settings.clientCertificateAlias - String host = settings.getHost(); - int port = settings.getPort(); - String clientCertificateAlias = settings.getClientCertificateAlias(); - - if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) { - Timber.d("Connecting to %s as %s", host, address); + if (K9MailLib.isDebug() && K9MailLib.DEBUG_PROTOCOL_IMAP) { + Timber.d("Connecting to %s as %s", host, address) } - SocketAddress socketAddress = new InetSocketAddress(address, port); - - Socket socket; - if (settings.getConnectionSecurity() == ConnectionSecurity.SSL_TLS_REQUIRED) { - socket = socketFactory.createSocket(null, host, port, clientCertificateAlias); + val socketAddress: SocketAddress = InetSocketAddress(address, port) + val socket = if (settings.connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { + socketFactory.createSocket(null, host, port, clientCertificateAlias) } else { - socket = new Socket(); + Socket() } - socket.connect(socketAddress, socketConnectTimeout); + socket.connect(socketAddress, socketConnectTimeout) - return socket; + return socket } - private void configureSocket() throws SocketException { - setSocketDefaultReadTimeout(); + private fun configureSocket() { + setSocketDefaultReadTimeout() } - @Override - public void setSocketDefaultReadTimeout() throws SocketException { - setSocketReadTimeout(socketReadTimeout); + override fun setSocketDefaultReadTimeout() { + setSocketReadTimeout(socketReadTimeout) } - @Override - public synchronized void setSocketReadTimeout(int timeout) throws SocketException { - if (socket != null) { - socket.setSoTimeout(timeout); - } + @Synchronized + override fun setSocketReadTimeout(timeout: Int) { + socket?.soTimeout = timeout } - private void setUpStreamsAndParserFromSocket() throws IOException { - setUpStreamsAndParser(socket.getInputStream(), socket.getOutputStream()); + private fun setUpStreamsAndParserFromSocket() { + val socket = checkNotNull(socket) + + setUpStreamsAndParser(socket.getInputStream(), socket.getOutputStream()) } - private void setUpStreamsAndParser(InputStream input, OutputStream output) { - inputStream = new PeekableInputStream(new BufferedInputStream(input, BUFFER_SIZE)); - responseParser = new ImapResponseParser(inputStream); - outputStream = new BufferedOutputStream(output, BUFFER_SIZE); + private fun setUpStreamsAndParser(input: InputStream, output: OutputStream) { + inputStream = PeekableInputStream(BufferedInputStream(input, BUFFER_SIZE)) + responseParser = ImapResponseParser(inputStream) + imapOutputStream = BufferedOutputStream(output, BUFFER_SIZE) } - private void readInitialResponse() throws IOException { - ImapResponse initialResponse = responseParser.readResponse(); - if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) { - Timber.v("%s <<< %s", getLogId(), initialResponse); + private fun readInitialResponse() { + val responseParser = checkNotNull(responseParser) + + val initialResponse = responseParser.readResponse() + + if (K9MailLib.isDebug() && K9MailLib.DEBUG_PROTOCOL_IMAP) { + Timber.v("%s <<< %s", logId, initialResponse) } - extractCapabilities(Collections.singletonList(initialResponse)); + + extractCapabilities(listOf(initialResponse)) } - private boolean extractCapabilities(List responses) { - CapabilityResponse capabilityResponse = CapabilityResponse.parse(responses); - if (capabilityResponse == null) { - return false; - } + private fun extractCapabilities(responses: List): Boolean { + val capabilityResponse = CapabilityResponse.parse(responses) ?: return false + val receivedCapabilities = capabilityResponse.capabilities - Set receivedCapabilities = capabilityResponse.getCapabilities(); - Timber.d("Saving %s capabilities for %s", receivedCapabilities, getLogId()); - capabilities = receivedCapabilities; + Timber.d("Saving %s capabilities for %s", receivedCapabilities, logId) + capabilities = receivedCapabilities - return true; + return true } - private void extractOrRequestCapabilities(List responses) throws IOException, MessagingException { + private fun extractOrRequestCapabilities(responses: List) { if (!extractCapabilities(responses)) { - Timber.i("Did not get capabilities in post-auth banner, requesting CAPABILITY for %s", getLogId()); - requestCapabilities(); + Timber.i("Did not get capabilities in post-auth banner, requesting CAPABILITY for %s", logId) + requestCapabilities() } } - private void requestCapabilitiesIfNecessary() throws IOException, MessagingException { - if (!capabilities.isEmpty()) { - return; - } + private fun requestCapabilitiesIfNecessary() { + if (capabilities.isNotEmpty()) return + if (K9MailLib.isDebug()) { - Timber.i("Did not get capabilities in banner, requesting CAPABILITY for %s", getLogId()); + Timber.i("Did not get capabilities in banner, requesting CAPABILITY for %s", logId) } - requestCapabilities(); + + requestCapabilities() } - private void requestCapabilities() throws IOException, MessagingException { - if (!extractCapabilities(executeSimpleCommand(Commands.CAPABILITY))) { - throw new MessagingException("Invalid CAPABILITY response received"); + private fun requestCapabilities() { + val responses = executeSimpleCommand(Commands.CAPABILITY) + + if (!extractCapabilities(responses)) { + throw MessagingException("Invalid CAPABILITY response received") } } - private void upgradeToTlsIfNecessary() throws IOException, MessagingException, GeneralSecurityException { - if (settings.getConnectionSecurity() == STARTTLS_REQUIRED) { - upgradeToTls(); + private fun upgradeToTlsIfNecessary() { + if (settings.connectionSecurity == ConnectionSecurity.STARTTLS_REQUIRED) { + upgradeToTls() } } - private void upgradeToTls() throws IOException, MessagingException, GeneralSecurityException { + private fun upgradeToTls() { if (!hasCapability(Capabilities.STARTTLS)) { /* * This exception triggers a "Certificate error" @@ -328,201 +282,216 @@ class RealImapConnection implements ImapConnection { * the account was configured with an obsolete * "STARTTLS (if available)" setting. */ - throw new CertificateValidationException("STARTTLS connection security not available"); + throw CertificateValidationException("STARTTLS connection security not available") } - startTLS(); + startTls() } - private void startTLS() throws IOException, MessagingException, GeneralSecurityException { - executeSimpleCommand(Commands.STARTTLS); + private fun startTls() { + executeSimpleCommand(Commands.STARTTLS) - String host = settings.getHost(); - int port = settings.getPort(); - String clientCertificateAlias = settings.getClientCertificateAlias(); + val host = settings.host + val port = settings.port + val clientCertificateAlias = settings.clientCertificateAlias + socket = socketFactory.createSocket(socket, host, port, clientCertificateAlias) - socket = socketFactory.createSocket(socket, host, port, clientCertificateAlias); - configureSocket(); - setUpStreamsAndParserFromSocket(); + configureSocket() + setUpStreamsAndParserFromSocket() // Per RFC 2595 (3.1): Once TLS has been started, reissue CAPABILITY command if (K9MailLib.isDebug()) { - Timber.i("Updating capabilities after STARTTLS for %s", getLogId()); + Timber.i("Updating capabilities after STARTTLS for %s", logId) } - requestCapabilities(); + requestCapabilities() } - private List authenticate() throws MessagingException, IOException { - switch (settings.getAuthType()) { - case XOAUTH2: + private fun authenticate(): List { + return when (settings.authType) { + AuthType.XOAUTH2 -> { if (oauthTokenProvider == null) { - throw new MessagingException("No OAuthToken Provider available."); + throw MessagingException("No OAuthToken Provider available.") } else if (!hasCapability(Capabilities.SASL_IR)) { - throw new MessagingException("SASL-IR capability is missing."); + throw MessagingException("SASL-IR capability is missing.") } else if (hasCapability(Capabilities.AUTH_OAUTHBEARER)) { - return authWithOAuthToken(OAuthMethod.OAUTHBEARER); + authWithOAuthToken(OAuthMethod.OAUTHBEARER) } else if (hasCapability(Capabilities.AUTH_XOAUTH2)) { - return authWithOAuthToken(OAuthMethod.XOAUTH2); + authWithOAuthToken(OAuthMethod.XOAUTH2) } else { - throw new MessagingException("Server doesn't support SASL OAUTHBEARER or XOAUTH2."); + throw MessagingException("Server doesn't support SASL OAUTHBEARER or XOAUTH2.") } - case CRAM_MD5: { + } + AuthType.CRAM_MD5 -> { if (hasCapability(Capabilities.AUTH_CRAM_MD5)) { - return authCramMD5(); + authCramMD5() } else { - throw new MessagingException("Server doesn't support encrypted passwords using CRAM-MD5."); + throw MessagingException("Server doesn't support encrypted passwords using CRAM-MD5.") } } - case PLAIN: { + AuthType.PLAIN -> { if (hasCapability(Capabilities.AUTH_PLAIN)) { - return saslAuthPlainWithLoginFallback(); + saslAuthPlainWithLoginFallback() } else if (!hasCapability(Capabilities.LOGINDISABLED)) { - return login(); + login() } else { - throw new MessagingException("Server doesn't support unencrypted passwords using AUTH=PLAIN " + - "and LOGIN is disabled."); + throw MessagingException( + "Server doesn't support unencrypted passwords using AUTH=PLAIN and LOGIN is disabled." + ) } } - case EXTERNAL: { + AuthType.EXTERNAL -> { if (hasCapability(Capabilities.AUTH_EXTERNAL)) { - return saslAuthExternal(); + saslAuthExternal() } else { // Provide notification to user of a problem authenticating using client certificates - throw new CertificateValidationException(CertificateValidationException.Reason.MissingCapability); + throw CertificateValidationException(CertificateValidationException.Reason.MissingCapability) } } - default: { - throw new MessagingException("Unhandled authentication method found in the server settings (bug)."); + else -> { + throw MessagingException("Unhandled authentication method found in the server settings (bug).") } } } - private List authWithOAuthToken(OAuthMethod method) throws IOException, MessagingException { - retryOAuthWithNewToken = true; - try { - return attemptOAuth(method); - } catch (NegativeImapResponseException e) { - //TODO: Check response code so we don't needlessly invalidate the token. - oauthTokenProvider.invalidateToken(); + private fun authWithOAuthToken(method: OAuthMethod): List { + val oauthTokenProvider = checkNotNull(oauthTokenProvider) + retryOAuthWithNewToken = true + + return try { + attemptOAuth(method) + } catch (e: NegativeImapResponseException) { + // TODO: Check response code so we don't needlessly invalidate the token. + oauthTokenProvider.invalidateToken() if (!retryOAuthWithNewToken) { - throw handlePermanentOAuthFailure(e); + throw handlePermanentOAuthFailure(e) } else { - return handleTemporaryOAuthFailure(method, e); + handleTemporaryOAuthFailure(method, e) } } } - private AuthenticationFailedException handlePermanentOAuthFailure(NegativeImapResponseException e) { - Timber.v(e, "Permanent failure during authentication using OAuth token"); - return new AuthenticationFailedException(e.getMessage(), e, e.getAlertText()); + private fun handlePermanentOAuthFailure(e: NegativeImapResponseException): AuthenticationFailedException { + Timber.v(e, "Permanent failure during authentication using OAuth token") + + return AuthenticationFailedException(message = e.message!!, throwable = e, messageFromServer = e.alertText) } - private List handleTemporaryOAuthFailure(OAuthMethod method, NegativeImapResponseException e) - throws IOException, MessagingException { - //We got a response indicating a retry might succeed after token refresh - //We could avoid this if we had a reasonable chance of knowing - //if a token was invalid before use (e.g. due to expiry). But we don't - //This is the intended behaviour per AccountManager + private fun handleTemporaryOAuthFailure(method: OAuthMethod, e: NegativeImapResponseException): List { + val oauthTokenProvider = checkNotNull(oauthTokenProvider) - Timber.v(e, "Temporary failure - retrying with new token"); - try { - return attemptOAuth(method); - } catch (NegativeImapResponseException e2) { - //Okay, we failed on a new token. - //Invalidate the token anyway but assume it's permanent. - Timber.v(e, "Authentication exception for new token, permanent error assumed"); - oauthTokenProvider.invalidateToken(); - throw handlePermanentOAuthFailure(e2); + // We got a response indicating a retry might succeed after token refresh + // We could avoid this if we had a reasonable chance of knowing + // if a token was invalid before use (e.g. due to expiry). But we don't + // This is the intended behaviour per AccountManager + Timber.v(e, "Temporary failure - retrying with new token") + + return try { + attemptOAuth(method) + } catch (e2: NegativeImapResponseException) { + // Okay, we failed on a new token. + // Invalidate the token anyway but assume it's permanent. + Timber.v(e, "Authentication exception for new token, permanent error assumed") + + oauthTokenProvider.invalidateToken() + + throw handlePermanentOAuthFailure(e2) } } - private List attemptOAuth(OAuthMethod method) throws MessagingException, IOException { - String token = oauthTokenProvider.getToken(OAuth2TokenProvider.OAUTH2_TIMEOUT); - String authString = method.buildInitialClientResponse(settings.getUsername(), token); - String tag = sendSaslIrCommand(method.getCommand(), authString, true); + private fun attemptOAuth(method: OAuthMethod): List { + val oauthTokenProvider = checkNotNull(oauthTokenProvider) + val responseParser = checkNotNull(responseParser) + + val token = oauthTokenProvider.getToken(OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong()) + + val authString = method.buildInitialClientResponse(settings.username, token) + val tag = sendSaslIrCommand(method.command, authString, true) - return responseParser.readStatusResponse(tag, method.getCommand(), getLogId(), - new UntaggedHandler() { - @Override - public void handleAsyncUntaggedResponse(ImapResponse response) throws IOException { - handleOAuthUntaggedResponse(response); - } - }); + return responseParser.readStatusResponse(tag, method.command, logId, ::handleOAuthUntaggedResponse) } - private void handleOAuthUntaggedResponse(ImapResponse response) throws IOException { - if (!response.isContinuationRequested()) { - return; - } + private fun handleOAuthUntaggedResponse(response: ImapResponse) { + if (!response.isContinuationRequested) return + + val imapOutputStream = checkNotNull(imapOutputStream) if (response.isString(0)) { - retryOAuthWithNewToken = XOAuth2ChallengeParser.shouldRetry(response.getString(0), settings.getHost()); + retryOAuthWithNewToken = XOAuth2ChallengeParser.shouldRetry(response.getString(0), settings.host) } - outputStream.write("\r\n".getBytes()); - outputStream.flush(); + imapOutputStream.write('\r'.code) + imapOutputStream.write('\n'.code) + imapOutputStream.flush() } - private List authCramMD5() throws MessagingException, IOException { - String command = Commands.AUTHENTICATE_CRAM_MD5; - String tag = sendCommand(command, false); + private fun authCramMD5(): List { + val command = Commands.AUTHENTICATE_CRAM_MD5 + val tag = sendCommand(command, false) - ImapResponse response = readContinuationResponse(tag); - if (response.size() != 1 || !(response.get(0) instanceof String)) { - throw new MessagingException("Invalid Cram-MD5 nonce received"); + val imapOutputStream = checkNotNull(imapOutputStream) + val responseParser = checkNotNull(responseParser) + + val response = readContinuationResponse(tag) + if (response.size != 1 || !response.isString(0)) { + throw MessagingException("Invalid Cram-MD5 nonce received") } - byte[] b64Nonce = response.getString(0).getBytes(); - byte[] b64CRAM = Authentication.computeCramMd5Bytes(settings.getUsername(), settings.getPassword(), b64Nonce); + val b64Nonce = response.getString(0).toByteArray() + val b64CRAM = Authentication.computeCramMd5Bytes(settings.username, settings.password, b64Nonce) - outputStream.write(b64CRAM); - outputStream.write('\r'); - outputStream.write('\n'); - outputStream.flush(); + imapOutputStream.write(b64CRAM) + imapOutputStream.write('\r'.code) + imapOutputStream.write('\n'.code) + imapOutputStream.flush() - try { - return responseParser.readStatusResponse(tag, command, getLogId(), null); - } catch (NegativeImapResponseException e) { - throw handleAuthenticationFailure(e); + return try { + responseParser.readStatusResponse(tag, command, logId, null) + } catch (e: NegativeImapResponseException) { + throw handleAuthenticationFailure(e) } } - private List saslAuthPlainWithLoginFallback() throws IOException, MessagingException { - try { - return saslAuthPlain(); - } catch (AuthenticationFailedException e) { - if (!isConnected()) { - throw e; + private fun saslAuthPlainWithLoginFallback(): List { + return try { + saslAuthPlain() + } catch (e: AuthenticationFailedException) { + if (!isConnected) { + throw e } - return login(); + login() } } - private List saslAuthPlain() throws IOException, MessagingException { - String command = Commands.AUTHENTICATE_PLAIN; - String tag = sendCommand(command, false); + private fun saslAuthPlain(): List { + val command = Commands.AUTHENTICATE_PLAIN + val tag = sendCommand(command, false) - readContinuationResponse(tag); + val imapOutputStream = checkNotNull(imapOutputStream) + val responseParser = checkNotNull(responseParser) - String credentials = "\000" + settings.getUsername() + "\000" + settings.getPassword(); - byte[] encodedCredentials = Base64.encodeBase64(credentials.getBytes()); + readContinuationResponse(tag) - outputStream.write(encodedCredentials); - outputStream.write('\r'); - outputStream.write('\n'); - outputStream.flush(); + val credentials = "\u0000" + settings.username + "\u0000" + settings.password + val encodedCredentials = Base64.encodeBase64(credentials.toByteArray()) - try { - return responseParser.readStatusResponse(tag, command, getLogId(), null); - } catch (NegativeImapResponseException e) { - throw handleAuthenticationFailure(e); + imapOutputStream.write(encodedCredentials) + imapOutputStream.write('\r'.code) + imapOutputStream.write('\n'.code) + imapOutputStream.flush() + + return try { + responseParser.readStatusResponse(tag, command, logId, null) + } catch (e: NegativeImapResponseException) { + throw handleAuthenticationFailure(e) } } - private List login() throws IOException, MessagingException { + private fun login(): List { + val password = checkNotNull(settings.password) + /* * Use quoted strings which permit spaces and quotes. (Using IMAP * string literals would be better, but some servers are broken @@ -530,24 +499,24 @@ class RealImapConnection implements ImapConnection { */ // escape double-quotes and backslash characters with a backslash - Pattern p = Pattern.compile("[\\\\\"]"); - String replacement = "\\\\$0"; - String username = p.matcher(settings.getUsername()).replaceAll(replacement); - String password = p.matcher(settings.getPassword()).replaceAll(replacement); + val pattern = Pattern.compile("[\\\\\"]") + val replacement = "\\\\$0" + val encodedUsername = pattern.matcher(settings.username).replaceAll(replacement) + val encodedPassword = pattern.matcher(password).replaceAll(replacement) - try { - String command = String.format(Commands.LOGIN + " \"%s\" \"%s\"", username, password); - return executeSimpleCommand(command, true); - } catch (NegativeImapResponseException e) { - throw handleAuthenticationFailure(e); + return try { + val command = String.format(Commands.LOGIN + " \"%s\" \"%s\"", encodedUsername, encodedPassword) + executeSimpleCommand(command, true) + } catch (e: NegativeImapResponseException) { + throw handleAuthenticationFailure(e) } } - private List saslAuthExternal() throws IOException, MessagingException { - try { - String command = Commands.AUTHENTICATE_EXTERNAL + " " + Base64.encode(settings.getUsername()); - return executeSimpleCommand(command, false); - } catch (NegativeImapResponseException e) { + private fun saslAuthExternal(): List { + return try { + val command = Commands.AUTHENTICATE_EXTERNAL + " " + Base64.encode(settings.username) + executeSimpleCommand(command, false) + } catch (e: NegativeImapResponseException) { /* * Provide notification to the user of a problem authenticating * using client certificates. We don't use an @@ -555,375 +524,375 @@ class RealImapConnection implements ImapConnection { * "Username or password incorrect" notification in * AccountSetupCheckSettings. */ - throw new CertificateValidationException(e.getMessage()); + throw CertificateValidationException(e.message) } } - private MessagingException handleAuthenticationFailure(NegativeImapResponseException e) { - ImapResponse lastResponse = e.getLastResponse(); - String responseCode = ResponseCodeExtractor.getResponseCode(lastResponse); + private fun handleAuthenticationFailure( + negativeResponseException: NegativeImapResponseException + ): MessagingException { + val lastResponse = negativeResponseException.lastResponse + val responseCode = ResponseCodeExtractor.getResponseCode(lastResponse) // If there's no response code we simply assume it was an authentication failure. - if (responseCode == null || responseCode.equals(ResponseCodeExtractor.AUTHENTICATION_FAILED)) { - if (e.wasByeResponseReceived()) { - close(); + return if (responseCode == null || responseCode == ResponseCodeExtractor.AUTHENTICATION_FAILED) { + if (negativeResponseException.wasByeResponseReceived()) { + close() } - return new AuthenticationFailedException(e.getMessage()); + AuthenticationFailedException(negativeResponseException.message!!) } else { - close(); - return e; + close() + + negativeResponseException } } - private void enableCompressionIfRequested() throws IOException, MessagingException { + private fun enableCompressionIfRequested() { if (hasCapability(Capabilities.COMPRESS_DEFLATE) && settings.useCompression()) { - enableCompression(); + enableCompression() } } - private void enableCompression() throws IOException, MessagingException { + private fun enableCompression() { try { - executeSimpleCommand(Commands.COMPRESS_DEFLATE); - } catch (NegativeImapResponseException e) { - Timber.d(e, "Unable to negotiate compression: "); - return; + executeSimpleCommand(Commands.COMPRESS_DEFLATE) + } catch (e: NegativeImapResponseException) { + Timber.d(e, "Unable to negotiate compression: ") + return } try { - InflaterInputStream input = new InflaterInputStream(socket.getInputStream(), new Inflater(true)); - ZOutputStream output = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_SPEED, true); - output.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + val socket = checkNotNull(socket) + val input = InflaterInputStream(socket.getInputStream(), Inflater(true)) + val output = ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_SPEED, true) + output.flushMode = JZlib.Z_PARTIAL_FLUSH - setUpStreamsAndParser(input, output); + setUpStreamsAndParser(input, output) if (K9MailLib.isDebug()) { - Timber.i("Compression enabled for %s", getLogId()); + Timber.i("Compression enabled for %s", logId) } - } catch (IOException e) { - close(); - Timber.e(e, "Error enabling compression"); + } catch (e: IOException) { + close() + Timber.e(e, "Error enabling compression") } } - private void retrievePathPrefixIfNecessary() throws IOException, MessagingException { - if (settings.getPathPrefix() != null) { - return; - } + private fun retrievePathPrefixIfNecessary() { + if (settings.pathPrefix != null) return if (hasCapability(Capabilities.NAMESPACE)) { if (K9MailLib.isDebug()) { - Timber.i("pathPrefix is unset and server has NAMESPACE capability"); + Timber.i("pathPrefix is unset and server has NAMESPACE capability") } - handleNamespace(); + + handleNamespace() } else { if (K9MailLib.isDebug()) { - Timber.i("pathPrefix is unset but server does not have NAMESPACE capability"); + Timber.i("pathPrefix is unset but server does not have NAMESPACE capability") } - settings.setPathPrefix(""); + + settings.pathPrefix = "" } } - private void handleNamespace() throws IOException, MessagingException { - List responses = executeSimpleCommand(Commands.NAMESPACE); + private fun handleNamespace() { + val responses = executeSimpleCommand(Commands.NAMESPACE) - NamespaceResponse namespaceResponse = NamespaceResponse.parse(responses); - if (namespaceResponse != null) { - String prefix = namespaceResponse.getPrefix(); - String hierarchyDelimiter = namespaceResponse.getHierarchyDelimiter(); + val namespaceResponse = NamespaceResponse.parse(responses) ?: return - settings.setPathPrefix(prefix); - settings.setPathDelimiter(hierarchyDelimiter); - settings.setCombinedPrefix(null); + settings.pathPrefix = namespaceResponse.prefix + settings.pathDelimiter = namespaceResponse.hierarchyDelimiter + settings.setCombinedPrefix(null) - if (K9MailLib.isDebug()) { - Timber.d("Got path '%s' and separator '%s'", prefix, hierarchyDelimiter); - } + if (K9MailLib.isDebug()) { + Timber.d("Got path '%s' and separator '%s'", namespaceResponse.prefix, namespaceResponse.hierarchyDelimiter) } } - private void retrievePathDelimiterIfNecessary() throws IOException, MessagingException { - if (settings.getPathDelimiter() == null) { - retrievePathDelimiter(); + private fun retrievePathDelimiterIfNecessary() { + if (settings.pathDelimiter == null) { + retrievePathDelimiter() } } - private void retrievePathDelimiter() throws IOException, MessagingException { - List listResponses; - try { - listResponses = executeSimpleCommand(Commands.LIST + " \"\" \"\""); - } catch (NegativeImapResponseException e) { - Timber.d(e, "Error getting path delimiter using LIST command"); - return; + private fun retrievePathDelimiter() { + val listResponses = try { + executeSimpleCommand(Commands.LIST + " \"\" \"\"") + } catch (e: NegativeImapResponseException) { + Timber.d(e, "Error getting path delimiter using LIST command") + return } - for (ImapResponse response : listResponses) { + for (response in listResponses) { if (isListResponse(response)) { - String hierarchyDelimiter = response.getString(2); - settings.setPathDelimiter(hierarchyDelimiter); - settings.setCombinedPrefix(null); + val hierarchyDelimiter = response.getString(2) + + settings.pathDelimiter = hierarchyDelimiter + settings.setCombinedPrefix(null) if (K9MailLib.isDebug()) { - Timber.d("Got path delimiter '%s' for %s", settings.getPathDelimiter(), getLogId()); + Timber.d("Got path delimiter '%s' for %s", hierarchyDelimiter, logId) } - break; + break } } } - private boolean isListResponse(ImapResponse response) { - boolean responseTooShort = response.size() < 4; - if (responseTooShort) { - return false; - } + private fun isListResponse(response: ImapResponse): Boolean { + if (response.size < 4) return false - boolean isListResponse = equalsIgnoreCase(response.get(0), Responses.LIST); - boolean hierarchyDelimiterValid = response.get(2) instanceof String; + val isListResponse = ImapResponseParser.equalsIgnoreCase(response[0], Responses.LIST) + val hierarchyDelimiterValid = response.isString(2) - return isListResponse && hierarchyDelimiterValid; + return isListResponse && hierarchyDelimiterValid } - @Override - public boolean hasCapability(@NotNull String capability) throws IOException, MessagingException { + override fun hasCapability(capability: String): Boolean { if (!open) { - open(); + open() } - return capabilities.contains(capability.toUpperCase(Locale.US)); + return capabilities.contains(capability.uppercase()) } - public boolean isCondstoreCapable() throws IOException, MessagingException { - return hasCapability(Capabilities.CONDSTORE); - } + private val isCondstoreCapable: Boolean + get() = hasCapability(Capabilities.CONDSTORE) - @Override - public boolean isIdleCapable() { - if (K9MailLib.isDebug()) { - Timber.v("Connection %s has %d capabilities", getLogId(), capabilities.size()); - } + override val isIdleCapable: Boolean + get() { + if (K9MailLib.isDebug()) { + Timber.v("Connection %s has %d capabilities", logId, capabilities.size) + } - return capabilities.contains(Capabilities.IDLE); - } + return capabilities.contains(Capabilities.IDLE) + } - @Override - public boolean isUidPlusCapable() { - return capabilities.contains(Capabilities.UID_PLUS); - } + override val isUidPlusCapable: Boolean + get() = capabilities.contains(Capabilities.UID_PLUS) - @Override - public synchronized void close() { - if (!open) { - return; - } + @Synchronized + override fun close() { + if (!open) return - open = false; - stacktraceForClose = new Exception(); + open = false - IOUtils.closeQuietly(inputStream); - IOUtils.closeQuietly(outputStream); - IOUtils.closeQuietly(socket); + stacktraceForClose = Exception() - inputStream = null; - outputStream = null; - socket = null; - } + IOUtils.closeQuietly(inputStream) + IOUtils.closeQuietly(imapOutputStream) + IOUtils.closeQuietly(socket) - @Override - @NotNull - public synchronized OutputStream getOutputStream() { - return outputStream; + inputStream = null + imapOutputStream = null + socket = null } - @Override - @NotNull - public String getLogId() { - return "conn" + hashCode(); - } + override val logId: String + get() = "conn" + hashCode() - @Override - @NotNull - public synchronized List executeSimpleCommand(@NotNull String command) - throws IOException, MessagingException { - return executeSimpleCommand(command, false); + @Synchronized + @Throws(IOException::class, MessagingException::class) + override fun executeSimpleCommand(command: String): List { + return executeSimpleCommand(command, false) } - public List executeSimpleCommand(String command, boolean sensitive) throws IOException, - MessagingException { - String commandToLog = command; - + @Throws(IOException::class, MessagingException::class) + fun executeSimpleCommand(command: String, sensitive: Boolean): List { + var commandToLog = command if (sensitive && !K9MailLib.isDebugSensitive()) { - commandToLog = "*sensitive*"; + commandToLog = "*sensitive*" } - String tag = sendCommand(command, sensitive); + val tag = sendCommand(command, sensitive) - try { - return responseParser.readStatusResponse(tag, commandToLog, getLogId(), null); - } catch (IOException e) { - close(); - throw e; + val responseParser = checkNotNull(responseParser) + return try { + responseParser.readStatusResponse(tag, commandToLog, logId, null) + } catch (e: IOException) { + close() + throw e } } - @Override - @NotNull - public synchronized List executeCommandWithIdSet(@NotNull String commandPrefix, - @NotNull String commandSuffix, @NotNull Set ids) throws IOException, MessagingException { - - GroupedIds groupedIds = IdGrouper.groupIds(ids); - List splitCommands = ImapCommandSplitter.splitCommand( - commandPrefix, commandSuffix, groupedIds, getLineLengthLimit()); + @Synchronized + @Throws(IOException::class, MessagingException::class) + override fun executeCommandWithIdSet( + commandPrefix: String, + commandSuffix: String, + ids: Set + ): List { + val groupedIds = IdGrouper.groupIds(ids) + val splitCommands = ImapCommandSplitter.splitCommand( + commandPrefix, commandSuffix, groupedIds, lineLengthLimit + ) - List responses = new ArrayList<>(); - for (String splitCommand : splitCommands) { - responses.addAll(executeSimpleCommand(splitCommand)); + return splitCommands.flatMap { splitCommand -> + executeSimpleCommand(splitCommand) } - - return responses; } - public String sendSaslIrCommand(String command, String initialClientResponse, boolean sensitive) - throws IOException, MessagingException { + @Throws(IOException::class, MessagingException::class) + fun sendSaslIrCommand(command: String, initialClientResponse: String, sensitive: Boolean): String { try { - open(); + open() + + val outputStream = checkNotNull(imapOutputStream) - String tag = Integer.toString(nextCommandTag++); - String commandToSend = tag + " " + command + " " + initialClientResponse + "\r\n"; - outputStream.write(commandToSend.getBytes()); - outputStream.flush(); + val tag = (nextCommandTag++).toString() + val commandToSend = "$tag $command $initialClientResponse\r\n" - if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) { + outputStream.write(commandToSend.toByteArray()) + outputStream.flush() + + if (K9MailLib.isDebug() && K9MailLib.DEBUG_PROTOCOL_IMAP) { if (sensitive && !K9MailLib.isDebugSensitive()) { - Timber.v("%s>>> [Command Hidden, Enable Sensitive Debug Logging To Show]", getLogId()); + Timber.v("%s>>> [Command Hidden, Enable Sensitive Debug Logging To Show]", logId) } else { - Timber.v("%s>>> %s %s %s", getLogId(), tag, command, initialClientResponse); + Timber.v("%s>>> %s %s %s", logId, tag, command, initialClientResponse) } } - return tag; - } catch (IOException | MessagingException e) { - close(); - throw e; + return tag + } catch (e: IOException) { + close() + throw e + } catch (e: MessagingException) { + close() + throw e } } - @Override - @NotNull - public synchronized String sendCommand(@NotNull String command, boolean sensitive) - throws MessagingException, IOException { + @Synchronized + @Throws(MessagingException::class, IOException::class) + override fun sendCommand(command: String, sensitive: Boolean): String { try { - open(); + open() + + val outputStream = checkNotNull(imapOutputStream) + + val tag = (nextCommandTag++).toString() + val commandToSend = "$tag $command\r\n" - String tag = Integer.toString(nextCommandTag++); - String commandToSend = tag + " " + command + "\r\n"; - outputStream.write(commandToSend.getBytes()); - outputStream.flush(); + outputStream.write(commandToSend.toByteArray()) + outputStream.flush() - if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) { + if (K9MailLib.isDebug() && K9MailLib.DEBUG_PROTOCOL_IMAP) { if (sensitive && !K9MailLib.isDebugSensitive()) { - Timber.v("%s>>> [Command Hidden, Enable Sensitive Debug Logging To Show]", getLogId()); + Timber.v("%s>>> [Command Hidden, Enable Sensitive Debug Logging To Show]", logId) } else { - Timber.v("%s>>> %s %s", getLogId(), tag, command); + Timber.v("%s>>> %s %s", logId, tag, command) } } - return tag; - } catch (IOException | MessagingException e) { - close(); - throw e; + return tag + } catch (e: IOException) { + close() + throw e + } catch (e: MessagingException) { + close() + throw e } } - @Override - public synchronized void sendContinuation(@NotNull String continuation) throws IOException { - outputStream.write(continuation.getBytes()); - outputStream.write('\r'); - outputStream.write('\n'); - outputStream.flush(); + @Synchronized + @Throws(IOException::class) + override fun sendContinuation(continuation: String) { + val outputStream = checkNotNull(imapOutputStream) - if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) { - Timber.v("%s>>> %s", getLogId(), continuation); + outputStream.write(continuation.toByteArray()) + outputStream.write('\r'.code) + outputStream.write('\n'.code) + outputStream.flush() + + if (K9MailLib.isDebug() && K9MailLib.DEBUG_PROTOCOL_IMAP) { + Timber.v("%s>>> %s", logId, continuation) } } - @Override - @NotNull - public ImapResponse readResponse() throws IOException { - return readResponse(null); + @Throws(IOException::class) + override fun readResponse(): ImapResponse { + return readResponse(null) } - @Override - @NotNull - public ImapResponse readResponse(@Nullable ImapResponseCallback callback) throws IOException { + @Throws(IOException::class) + override fun readResponse(callback: ImapResponseCallback?): ImapResponse { try { - ImapResponse response = responseParser.readResponse(callback); + val responseParser = checkNotNull(responseParser) + + val response = responseParser.readResponse(callback) - if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) { - Timber.v("%s<<<%s", getLogId(), response); + if (K9MailLib.isDebug() && K9MailLib.DEBUG_PROTOCOL_IMAP) { + Timber.v("%s<<<%s", logId, response) } - return response; - } catch (IOException e) { - close(); - throw e; + return response + } catch (e: IOException) { + close() + throw e } } - private ImapResponse readContinuationResponse(String tag) throws IOException, MessagingException { - ImapResponse response; + private fun readContinuationResponse(tag: String): ImapResponse { + var response: ImapResponse do { - response = readResponse(); + response = readResponse() - String responseTag = response.getTag(); + val responseTag = response.tag if (responseTag != null) { - if (responseTag.equalsIgnoreCase(tag)) { - throw new MessagingException("Command continuation aborted: " + response); + if (responseTag.equals(tag, ignoreCase = true)) { + throw MessagingException("Command continuation aborted: $response") } else { - Timber.w("After sending tag %s, got tag response from previous command %s for %s", - tag, response, getLogId()); + Timber.w( + "After sending tag %s, got tag response from previous command %s for %s", + tag, response, logId + ) } } - } while (!response.isContinuationRequested()); + } while (!response.isContinuationRequested) - return response; + return response } - int getLineLengthLimit() throws IOException, MessagingException { - return isCondstoreCapable() ? LENGTH_LIMIT_WITH_CONDSTORE : LENGTH_LIMIT_WITHOUT_CONDSTORE; - } - - @Override - public int getConnectionGeneration() { - return connectionGeneration; - } + @get:Throws(IOException::class, MessagingException::class) + val lineLengthLimit: Int + get() = if (isCondstoreCapable) LENGTH_LIMIT_WITH_CONDSTORE else LENGTH_LIMIT_WITHOUT_CONDSTORE - - private enum OAuthMethod { + private enum class OAuthMethod { XOAUTH2 { - @Override - String getCommand() { - return Commands.AUTHENTICATE_XOAUTH2; - } + override val command: String = Commands.AUTHENTICATE_XOAUTH2 - @Override - String buildInitialClientResponse(String username, String token) { - return Authentication.computeXoauth(username, token); + override fun buildInitialClientResponse(username: String, token: String): String { + return Authentication.computeXoauth(username, token) } }, OAUTHBEARER { - @Override - String getCommand() { - return Commands.AUTHENTICATE_OAUTHBEARER; - } + override val command: String = Commands.AUTHENTICATE_OAUTHBEARER - @Override - String buildInitialClientResponse(String username, String token) { - return OAuthBearer.buildOAuthBearerInitialClientResponse(username, token); + override fun buildInitialClientResponse(username: String, token: String): String { + return buildOAuthBearerInitialClientResponse(username, token) } }; - abstract String getCommand(); - abstract String buildInitialClientResponse(String username, String token); + abstract val command: String + abstract fun buildInitialClientResponse(username: String, token: String): String + } + + companion object { + private const val BUFFER_SIZE = 1024 + + /* The below limits are 20 octets less than the recommended limits, in order to compensate for + * the length of the command tag, the space after the tag and the CRLF at the end of the command + * (these are not taken into account when calculating the length of the command). For more + * information, refer to section 4 of RFC 7162. + * + * The length limit for servers supporting the CONDSTORE extension is large in order to support + * the QRESYNC parameter to the SELECT/EXAMINE commands, which accept a list of known message + * sequence numbers as well as their corresponding UIDs. + */ + private const val LENGTH_LIMIT_WITHOUT_CONDSTORE = 980 + private const val LENGTH_LIMIT_WITH_CONDSTORE = 8172 } } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt index 9a404ff48a..d6a004623d 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt @@ -1011,9 +1011,9 @@ class RealImapConnectionTest { settings, socketFactory, oAuth2TokenProvider, + connectionGeneration, SOCKET_CONNECT_TIMEOUT, - SOCKET_READ_TIMEOUT, - connectionGeneration + SOCKET_READ_TIMEOUT ) } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt index 5b2251fc57..b7e6d7d3b0 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt @@ -29,7 +29,6 @@ import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.anySet import org.mockito.ArgumentMatchers.anyString -import org.mockito.ArgumentMatchers.eq import org.mockito.ArgumentMatchers.startsWith import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.times @@ -39,6 +38,7 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -48,7 +48,7 @@ class RealImapFolderTest { override fun getCombinedPrefix() = "" override fun getPermanentFlagsIndex() = mutableSetOf() } - private val imapConnection = mock() + private val imapConnection = mock() private val testConnectionManager = TestConnectionManager(imapConnection) private lateinit var tempDirectory: File -- GitLab From 89d3df091fda2a035f7dbf6e5ab11af4aee49a6e Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 12 Aug 2022 09:34:01 +0200 Subject: [PATCH 30/85] Change `ImapSettings.useCompression` to a property --- .../main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt | 2 +- .../java/com/fsck/k9/mail/store/imap/RealImapConnection.kt | 2 +- .../main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt | 5 ++--- .../java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt | 3 +-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt index b5cf301975..03e7582b5b 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.kt @@ -14,7 +14,7 @@ internal interface ImapSettings { val username: String val password: String? val clientCertificateAlias: String? - fun useCompression(): Boolean + val useCompression: Boolean var pathPrefix: String? var pathDelimiter: String? diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt index 8798a230d7..71c8d86324 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt @@ -549,7 +549,7 @@ internal class RealImapConnection( } private fun enableCompressionIfRequested() { - if (hasCapability(Capabilities.COMPRESS_DEFLATE) && settings.useCompression()) { + if (hasCapability(Capabilities.COMPRESS_DEFLATE) && settings.useCompression) { enableCompression() } } diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt index 9d67714a20..08a907e8e5 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt @@ -296,9 +296,8 @@ internal open class RealImapStore( override val password: String? = serverSettings.password override val clientCertificateAlias: String? = serverSettings.clientCertificateAlias - override fun useCompression(): Boolean { - return this@RealImapStore.config.useCompression() - } + override val useCompression: Boolean + get() = this@RealImapStore.config.useCompression() override var pathPrefix: String? get() = this@RealImapStore.pathPrefix diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt index 5a74db0b92..5e75c230d3 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.kt @@ -10,10 +10,9 @@ internal class SimpleImapSettings( override val authType: AuthType, override val username: String, override val password: String? = null, - private val useCompression: Boolean = false + override val useCompression: Boolean = false ) : ImapSettings { override val clientCertificateAlias: String? = null - override fun useCompression(): Boolean = useCompression override var pathPrefix: String? = null override var pathDelimiter: String? = null -- GitLab From ec76473a1c20367e01c1e2168e6497f0502cddcc Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 12 Aug 2022 09:39:01 +0200 Subject: [PATCH 31/85] Remove `RealImapConnection.handleConnectException()` --- .../k9/mail/store/imap/RealImapConnection.kt | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt index 71c8d86324..363640466d 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt @@ -23,7 +23,6 @@ import java.io.BufferedOutputStream import java.io.IOException import java.io.InputStream import java.io.OutputStream -import java.net.ConnectException import java.net.InetAddress import java.net.InetSocketAddress import java.net.Socket @@ -101,8 +100,6 @@ internal class RealImapConnection( retrievePathDelimiterIfNecessary() } catch (e: SSLException) { handleSslException(e) - } catch (e: ConnectException) { - handleConnectException(e) } catch (e: GeneralSecurityException) { throw MessagingException("Unable to open connection to IMAP server due to security error.", e) } finally { @@ -121,20 +118,6 @@ internal class RealImapConnection( } } - // TODO: Remove this. There is no documentation on why this was added, there are no tests, and this is unlikely to - // still work. - private fun handleConnectException(e: ConnectException) { - val message = e.message ?: throw e - - val tokens = message.split("-") - if (tokens.size > 1) { - Timber.e(e, "Stripping host/port from ConnectionException for %s", logId) - throw ConnectException(tokens[1].trim()) - } else { - throw e - } - } - @get:Synchronized override val isConnected: Boolean get() { -- GitLab From fd396b183debe084a5cbdfba260bec1f6640bdbf Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 25 Aug 2022 16:01:17 +0200 Subject: [PATCH 32/85] Don't crash when trying to access attachment (meta) data --- .../loader/AttachmentContentLoader.java | 3 +- .../activity/loader/AttachmentInfoLoader.java | 103 ++++++++++-------- 2 files changed, 56 insertions(+), 50 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentContentLoader.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentContentLoader.java index 6a420c812a..19ceff5b66 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentContentLoader.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentContentLoader.java @@ -3,7 +3,6 @@ package com.fsck.k9.activity.loader; import java.io.File; import java.io.FileOutputStream; -import java.io.IOException; import java.io.InputStream; import android.content.ContentResolver; @@ -88,7 +87,7 @@ public class AttachmentContentLoader extends AsyncTaskLoader { cachedResultAttachment = sourceAttachment.deriveWithLoadComplete(file.getAbsolutePath()); return cachedResultAttachment; - } catch (IOException e) { + } catch (Exception e) { Timber.e(e, "Error saving attachment!"); } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentInfoLoader.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentInfoLoader.java index 6750dd52b3..9bbf2e61dc 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentInfoLoader.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentInfoLoader.java @@ -46,63 +46,70 @@ public class AttachmentInfoLoader extends AsyncTaskLoader { @Override public Attachment loadInBackground() { - Uri uri = sourceAttachment.uri; - String contentType = sourceAttachment.contentType; - - long size = -1; - String name = null; - - ContentResolver contentResolver = getContext().getContentResolver(); - - Cursor metadataCursor = contentResolver.query( - uri, - new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }, - null, - null, - null); - - if (metadataCursor != null) { - try { - if (metadataCursor.moveToFirst()) { - name = metadataCursor.getString(0); - size = metadataCursor.getInt(1); + try { + Uri uri = sourceAttachment.uri; + String contentType = sourceAttachment.contentType; + + long size = -1; + String name = null; + + ContentResolver contentResolver = getContext().getContentResolver(); + + Cursor metadataCursor = contentResolver.query( + uri, + new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }, + null, + null, + null); + + if (metadataCursor != null) { + try { + if (metadataCursor.moveToFirst()) { + name = metadataCursor.getString(0); + size = metadataCursor.getInt(1); + } + } finally { + metadataCursor.close(); } - } finally { - metadataCursor.close(); } - } - if (name == null) { - name = uri.getLastPathSegment(); - } + if (name == null) { + name = uri.getLastPathSegment(); + } - String usableContentType = contentResolver.getType(uri); - if (usableContentType == null && contentType != null && contentType.indexOf('*') != -1) { - usableContentType = contentType; - } + String usableContentType = contentResolver.getType(uri); + if (usableContentType == null && contentType != null && contentType.indexOf('*') != -1) { + usableContentType = contentType; + } - if (usableContentType == null) { - usableContentType = MimeTypeUtil.getMimeTypeByExtension(name); - } + if (usableContentType == null) { + usableContentType = MimeTypeUtil.getMimeTypeByExtension(name); + } - if (!sourceAttachment.allowMessageType && MimeUtility.isMessageType(usableContentType)) { - usableContentType = MimeTypeUtil.DEFAULT_ATTACHMENT_MIME_TYPE; - } + if (!sourceAttachment.allowMessageType && MimeUtility.isMessageType(usableContentType)) { + usableContentType = MimeTypeUtil.DEFAULT_ATTACHMENT_MIME_TYPE; + } - if (size <= 0) { - String uriString = uri.toString(); - if (uriString.startsWith("file://")) { - File f = new File(uriString.substring("file://".length())); - size = f.length(); + if (size <= 0) { + String uriString = uri.toString(); + if (uriString.startsWith("file://")) { + File f = new File(uriString.substring("file://".length())); + size = f.length(); + } else { + Timber.v("Not a file: %s", uriString); + } } else { - Timber.v("Not a file: %s", uriString); + Timber.v("old attachment.size: %d", size); } - } else { - Timber.v("old attachment.size: %d", size); - } - Timber.v("new attachment.size: %d", size); + Timber.v("new attachment.size: %d", size); + + cachedResultAttachment = sourceAttachment.deriveWithMetadataLoaded(usableContentType, name, size); + return cachedResultAttachment; + } catch (Exception e) { + Timber.e(e, "Error getting attachment meta data"); - cachedResultAttachment = sourceAttachment.deriveWithMetadataLoaded(usableContentType, name, size); - return cachedResultAttachment; + cachedResultAttachment = sourceAttachment.deriveWithLoadCancelled(); + return cachedResultAttachment; + } } } -- GitLab From 27d1dd38288dac5be0821ca419e95bf6eb708df7 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 25 Aug 2022 16:29:12 +0200 Subject: [PATCH 33/85] Keep 'align' attribute of 'div' elements --- .../app/k9mail/html/cleaner/BodyCleaner.kt | 1 + .../k9mail/html/cleaner/HtmlSanitizerTest.kt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt index 80fa043e84..2d9c493f90 100644 --- a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt @@ -15,6 +15,7 @@ internal class BodyCleaner { val allowList = Safelist.relaxed() .addTags("font", "hr", "ins", "del", "center", "map", "area", "title") .addAttributes("font", "color", "face", "size") + .addAttributes("div", "align") .addAttributes( "table", "align", "background", "bgcolor", "border", "cellpadding", "cellspacing", "width" diff --git a/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt b/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt index 12ea9e797b..1faf36d2ea 100644 --- a/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt +++ b/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt @@ -409,6 +409,24 @@ class HtmlSanitizerTest { ) } + @Test + fun `should keep 'align' attribute on 'div' element`() { + val html = """
text
""" + + val result = htmlSanitizer.sanitize(html) + + assertThat(result.toCompactString()).isEqualTo( + """ + + + +
text
+ + + """.trimIndent().trimLineBreaks() + ) + } + private fun Document.toCompactString(): String { outputSettings() .prettyPrint(false) -- GitLab From c0be8a800076d2276676085091da948fe4587d4e Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 25 Aug 2022 16:35:48 +0200 Subject: [PATCH 34/85] Keep 'name' attribute of 'a' elements --- .../app/k9mail/html/cleaner/BodyCleaner.kt | 1 + .../k9mail/html/cleaner/HtmlSanitizerTest.kt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt index 2d9c493f90..5697b115c4 100644 --- a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt @@ -15,6 +15,7 @@ internal class BodyCleaner { val allowList = Safelist.relaxed() .addTags("font", "hr", "ins", "del", "center", "map", "area", "title") .addAttributes("font", "color", "face", "size") + .addAttributes("a", "name") .addAttributes("div", "align") .addAttributes( "table", "align", "background", "bgcolor", "border", "cellpadding", "cellspacing", diff --git a/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt b/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt index 1faf36d2ea..8720a3273d 100644 --- a/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt +++ b/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt @@ -427,6 +427,24 @@ class HtmlSanitizerTest { ) } + @Test + fun `should keep 'name' attribute on 'a' element`() { + val html = """""" + + val result = htmlSanitizer.sanitize(html) + + assertThat(result.toCompactString()).isEqualTo( + """ + + + + + + + """.trimIndent().trimLineBreaks() + ) + } + private fun Document.toCompactString(): String { outputSettings() .prettyPrint(false) -- GitLab From 7381cb808d5a4b53c7842af026f2311fded9c747 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 25 Aug 2022 22:03:37 +0200 Subject: [PATCH 35/85] Fix test in `MimeParameterDecoderTest` --- .../com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt index a5cc35e232..31fddaa3ad 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt @@ -100,8 +100,8 @@ class MimeParameterDecoderTest { val mimeValue = MimeParameterDecoder.decode( "application/x-stuff;\r\n" + " name*0=\"[one]\";\r\n" + - " name*1=\"[two]\";\r\n" + - " name*2=\"[three]\"" + " NAME*1=\"[two]\";\r\n" + + " nAmE*2=\"[three]\"" ) assertParametersEquals(mimeValue, "name" to "[one][two][three]") -- GitLab From ef91c6d3ff53c5963882ff1f78014febf7f095c0 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 25 Aug 2022 22:22:46 +0200 Subject: [PATCH 36/85] Convert `MimeParameterDecoderTest` to using Truth --- .../mail/internet/MimeParameterDecoderTest.kt | 180 +++++++++--------- 1 file changed, 85 insertions(+), 95 deletions(-) diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt index 31fddaa3ad..def1fe9890 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt @@ -1,7 +1,8 @@ package com.fsck.k9.mail.internet -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue +import com.google.common.truth.MapSubject +import com.google.common.truth.Ordered +import com.google.common.truth.Truth.assertThat import org.junit.Test class MimeParameterDecoderTest { @@ -9,18 +10,18 @@ class MimeParameterDecoderTest { fun rfc2045_example1() { val mimeValue = MimeParameterDecoder.decode("text/plain; charset=us-ascii (Plain text)") - assertEquals("text/plain", mimeValue.value) - assertParametersEquals(mimeValue, "charset" to "us-ascii") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.value).isEqualTo("text/plain") + assertThat(mimeValue.parameters).containsExactlyEntries("charset" to "us-ascii") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun rfc2045_example2() { val mimeValue = MimeParameterDecoder.decode("text/plain; charset=\"us-ascii\"") - assertEquals("text/plain", mimeValue.value) - assertParametersEquals(mimeValue, "charset" to "us-ascii") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.value).isEqualTo("text/plain") + assertThat(mimeValue.parameters).containsExactlyEntries("charset" to "us-ascii") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -31,13 +32,12 @@ class MimeParameterDecoderTest { " URL*1=\"cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar\"" ) - assertEquals("message/external-body", mimeValue.value) - assertParametersEquals( - mimeValue, + assertThat(mimeValue.value).isEqualTo("message/external-body") + assertThat(mimeValue.parameters).containsExactlyEntries( "access-type" to "URL", "url" to "ftp://cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar" ) - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -47,13 +47,12 @@ class MimeParameterDecoderTest { " URL=\"ftp://cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar\"" ) - assertEquals("message/external-body", mimeValue.value) - assertParametersEquals( - mimeValue, + assertThat(mimeValue.value).isEqualTo("message/external-body") + assertThat(mimeValue.parameters).containsExactlyEntries( "access-type" to "URL", "url" to "ftp://cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar" ) - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -63,9 +62,9 @@ class MimeParameterDecoderTest { " name*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A" ) - assertEquals("application/x-stuff", mimeValue.value) - assertParametersEquals(mimeValue, "name" to "This is ***fun***") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.value).isEqualTo("application/x-stuff") + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "This is ***fun***") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -77,9 +76,9 @@ class MimeParameterDecoderTest { " name*2=\"isn't it!\"" ) - assertEquals("application/x-stuff", mimeValue.value) - assertParametersEquals(mimeValue, "name" to "This is even more ***fun*** isn't it!") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.value).isEqualTo("application/x-stuff") + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "This is even more ***fun*** isn't it!") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -91,8 +90,8 @@ class MimeParameterDecoderTest { " name*0=\"[one]\"" ) - assertParametersEquals(mimeValue, "name" to "[one][two][three]") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "[one][two][three]") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -104,8 +103,8 @@ class MimeParameterDecoderTest { " nAmE*2=\"[three]\"" ) - assertParametersEquals(mimeValue, "name" to "[one][two][three]") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "[one][two][three]") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -120,8 +119,8 @@ class MimeParameterDecoderTest { " name*5=six" ) - assertParametersEquals(mimeValue, "name" to "[one][two][three][four][five]six") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "[one][two][three][four][five]six") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -132,8 +131,8 @@ class MimeParameterDecoderTest { " name*=utf-8''filen%C3%A4me.ext" ) - assertParametersEquals(mimeValue, "name" to "filenäme.ext") - assertIgnoredParametersEquals(mimeValue, "name" to "filename.ext") + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "filenäme.ext") + assertThat(mimeValue.ignoredParameters).containsExactly("name" to "filename.ext") } @Test @@ -145,8 +144,8 @@ class MimeParameterDecoderTest { " name=two" ) - assertParametersEquals(mimeValue, "extra" to "something") - assertIgnoredParametersEquals(mimeValue, "name" to "one", "name" to "two") + assertThat(mimeValue.parameters).containsExactlyEntries("extra" to "something") + assertThat(mimeValue.ignoredParameters).containsExactly("name" to "one", "name" to "two") } @Test @@ -158,26 +157,26 @@ class MimeParameterDecoderTest { " NAME=two" ) - assertParametersEquals(mimeValue, "extra" to "something") - assertIgnoredParametersEquals(mimeValue, "name" to "one", "name" to "two") + assertThat(mimeValue.parameters).containsExactlyEntries("extra" to "something") + assertThat(mimeValue.ignoredParameters).containsExactly("name" to "one", "name" to "two") } @Test fun name_only_parameter() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; parameter") - assertEquals(30, mimeValue.parserErrorIndex) - assertTrue(mimeValue.parameters.isEmpty()) - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parserErrorIndex).isEqualTo(30) + assertThat(mimeValue.parameters).isEmpty() + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun missing_parameter_value() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; parameter=") - assertEquals(31, mimeValue.parserErrorIndex) - assertTrue(mimeValue.parameters.isEmpty()) - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parserErrorIndex).isEqualTo(31) + assertThat(mimeValue.parameters).isEmpty() + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -189,72 +188,72 @@ class MimeParameterDecoderTest { " (comment) extra (comment) = (comment) something (comment)" ) - assertParametersEquals(mimeValue, "name" to "one", "extra" to "something") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "one", "extra" to "something") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun iso8859_1_charset() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*=iso-8859-1''filen%E4me.ext") - assertParametersEquals(mimeValue, "name" to "filenäme.ext") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "filenäme.ext") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun missing_charset() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*=''filen%AAme.ext") - assertParametersEquals(mimeValue, "name" to "filen%AAme.ext") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "filen%AAme.ext") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun unknown_charset() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*=foobar''filen%AAme.ext") - assertParametersEquals(mimeValue, "name" to "filen%AAme.ext") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "filen%AAme.ext") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun section_index_missing() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name**=utf-8''filename") - assertParametersEquals(mimeValue, "name**" to "utf-8''filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name**" to "utf-8''filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun section_index_not_a_number() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*x*=filename") - assertParametersEquals(mimeValue, "name*x*" to "filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*x*" to "filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun section_index_prefixed_with_plus() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*+0=filename") - assertParametersEquals(mimeValue, "name*+0" to "filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*+0" to "filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun section_index_prefixed_with_minus() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*-0=filename") - assertParametersEquals(mimeValue, "name*-0" to "filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*-0" to "filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun section_index_with_two_zeros() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*00=filename") - assertParametersEquals(mimeValue, "name*00" to "filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*00" to "filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -265,8 +264,8 @@ class MimeParameterDecoderTest { " name*01=two" ) - assertParametersEquals(mimeValue, "name" to "one", "name*01" to "two") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "one", "name*01" to "two") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -276,80 +275,80 @@ class MimeParameterDecoderTest { " name*10000000000000000000=filename" ) - assertParametersEquals(mimeValue, "name*10000000000000000000" to "filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*10000000000000000000" to "filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun extended_parameter_name_with_additional_asterisk() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*0**=utf-8''filename") - assertParametersEquals(mimeValue, "name*0**" to "utf-8''filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*0**" to "utf-8''filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun extended_parameter_name_with_additional_text() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*0*x=utf-8''filename") - assertParametersEquals(mimeValue, "name*0*x" to "utf-8''filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*0*x" to "utf-8''filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun extended_parameter_value_with_quoted_string() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*0*=\"utf-8''filename\"") - assertParametersEquals(mimeValue, "name*0*" to "utf-8''filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*0*" to "utf-8''filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun extended_initial_parameter_value_missing_single_quotes() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*0*=filename") - assertParametersEquals(mimeValue, "name*0*" to "filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*0*" to "filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun extended_initial_parameter_value_missing_second_single_quote() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*0*='") - assertParametersEquals(mimeValue, "name*0*" to "'") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*0*" to "'") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun extended_parameter_value_with_trailing_percent_sign() { val mimeValue = MimeParameterDecoder.decode("attachment; filename*=utf-8''file%") - assertParametersEquals(mimeValue, "filename*" to "utf-8''file%") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("filename*" to "utf-8''file%") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun extended_parameter_value_with_invalid_percent_encoding() { val mimeValue = MimeParameterDecoder.decode("attachment; filename*=UTF-8''f%oo.html") - assertParametersEquals(mimeValue, "filename*" to "UTF-8''f%oo.html") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("filename*" to "UTF-8''f%oo.html") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun section_0_missing() { val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name*1=filename") - assertParametersEquals(mimeValue, "name*1" to "filename") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name*1" to "filename") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test fun semicolon_in_parameter_value() { val mimeValue = MimeParameterDecoder.decode("attachment; filename=\"Here's a semicolon;.txt\"") - assertParametersEquals(mimeValue, "filename" to "Here's a semicolon;.txt") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("filename" to "Here's a semicolon;.txt") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -359,8 +358,8 @@ class MimeParameterDecoderTest { " name=\"=?UTF-8?Q?filn=C3=A4me=2Eext?=\"" ) - assertParametersEquals(mimeValue, "name" to "filnäme.ext") - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "filnäme.ext") + assertThat(mimeValue.ignoredParameters).isEmpty() } @Test @@ -372,23 +371,14 @@ class MimeParameterDecoderTest { " =?UTF-8?Q?non-ASCII_characters=3A_=C3=A4=E2=82=AC=F0=9F=8C=9E?=\"" ) - assertParametersEquals( - mimeValue, + assertThat(mimeValue.parameters).containsExactlyEntries( "name" to "File name that is so long it likes to be wrapped " + "into multiple lines. Also non-ASCII characters: ä€\uD83C\uDF1E" ) - assertTrue(mimeValue.ignoredParameters.isEmpty()) + assertThat(mimeValue.ignoredParameters).isEmpty() } - private fun assertParametersEquals(mimeValue: MimeValue, vararg expected: Pair) { - assertEquals(expected.toSet(), mimeValue.parameters.toPairSet()) - } - - private fun assertIgnoredParametersEquals(mimeValue: MimeValue, vararg expected: Pair) { - assertEquals(expected.toSet(), mimeValue.ignoredParameters.toSet()) - } - - private fun Map.toPairSet(): Set> { - return this.map { (key, value) -> key to value }.toSet() + private fun MapSubject.containsExactlyEntries(vararg values: Pair): Ordered { + return containsExactlyEntriesIn(values.toMap()) } } -- GitLab From 77396629cb5a6e91e54a39b274d1587f765f9bf5 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 29 Aug 2022 16:40:49 +0200 Subject: [PATCH 37/85] Add `MessageStore.hasMoreMessages()` --- .../k9/mailstore/FolderNotFoundException.kt | 3 ++ .../com/fsck/k9/mailstore/MessageStore.kt | 5 +++ .../k9/storage/messages/K9MessageStore.kt | 4 ++ .../messages/RetrieveFolderOperations.kt | 5 +++ .../messages/RetrieveFolderOperationsTest.kt | 40 +++++++++++++++++++ 5 files changed, 57 insertions(+) create mode 100644 app/core/src/main/java/com/fsck/k9/mailstore/FolderNotFoundException.kt diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/FolderNotFoundException.kt b/app/core/src/main/java/com/fsck/k9/mailstore/FolderNotFoundException.kt new file mode 100644 index 0000000000..34dcf1ca00 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/mailstore/FolderNotFoundException.kt @@ -0,0 +1,3 @@ +package com.fsck.k9.mailstore + +class FolderNotFoundException(val folderId: Long) : RuntimeException("Folder not found: $folderId") diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt index 9294ac736f..8d55d9149d 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt @@ -231,6 +231,11 @@ interface MessageStore { */ fun setNotificationClass(folderId: Long, folderClass: FolderClass) + /** + * Get the 'more messages' state of a folder. + */ + fun hasMoreMessages(folderId: Long): MoreMessages? + /** * Update the 'more messages' state of a folder. */ diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt index 2f617fa593..ceb7a12578 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt @@ -184,6 +184,10 @@ class K9MessageStore( updateFolderOperations.setNotificationClass(folderId, folderClass) } + override fun hasMoreMessages(folderId: Long): MoreMessages { + return retrieveFolderOperations.hasMoreMessages(folderId) + } + override fun setMoreMessages(folderId: Long, moreMessages: MoreMessages) { updateFolderOperations.setMoreMessages(folderId, moreMessages) } diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt index 7a68605927..44bbba649c 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveFolderOperations.kt @@ -8,6 +8,7 @@ import com.fsck.k9.mail.FolderClass import com.fsck.k9.mail.FolderType import com.fsck.k9.mailstore.FolderDetailsAccessor import com.fsck.k9.mailstore.FolderMapper +import com.fsck.k9.mailstore.FolderNotFoundException import com.fsck.k9.mailstore.LockableDatabase import com.fsck.k9.mailstore.MoreMessages import com.fsck.k9.mailstore.toFolderType @@ -157,6 +158,10 @@ internal class RetrieveFolderOperations(private val lockableDatabase: LockableDa } } } + + fun hasMoreMessages(folderId: Long): MoreMessages { + return getFolder(folderId) { it.moreMessages } ?: throw FolderNotFoundException(folderId) + } } private class CursorFolderAccessor(val cursor: Cursor) : FolderDetailsAccessor { diff --git a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt index 69fc55cbb5..234dc350ca 100644 --- a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt +++ b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveFolderOperationsTest.kt @@ -3,9 +3,12 @@ package com.fsck.k9.storage.messages import com.fsck.k9.Account.FolderMode import com.fsck.k9.mail.FolderClass import com.fsck.k9.mail.FolderType +import com.fsck.k9.mailstore.FolderNotFoundException +import com.fsck.k9.mailstore.MoreMessages import com.fsck.k9.mailstore.toDatabaseFolderType import com.fsck.k9.storage.RobolectricTest import com.google.common.truth.Truth.assertThat +import org.junit.Assert.fail import org.junit.Test class RetrieveFolderOperationsTest : RobolectricTest() { @@ -347,4 +350,41 @@ class RetrieveFolderOperationsTest : RobolectricTest() { assertThat(result).isEqualTo(2) } + + @Test + fun `get 'more messages' value from non-existent folder`() { + try { + retrieveFolderOperations.hasMoreMessages(23) + fail("Expected exception") + } catch (e: FolderNotFoundException) { + assertThat(e.folderId).isEqualTo(23) + } + } + + @Test + fun `get 'more messages' value from folder with value 'unknown'`() { + val folderId = sqliteDatabase.createFolder(moreMessages = "unknown") + + val result = retrieveFolderOperations.hasMoreMessages(folderId) + + assertThat(result).isEqualTo(MoreMessages.UNKNOWN) + } + + @Test + fun `get 'more messages' value from folder with value 'false'`() { + val folderId = sqliteDatabase.createFolder(moreMessages = "false") + + val result = retrieveFolderOperations.hasMoreMessages(folderId) + + assertThat(result).isEqualTo(MoreMessages.FALSE) + } + + @Test + fun `get 'more messages' value from folder with value 'true'`() { + val folderId = sqliteDatabase.createFolder(moreMessages = "true") + + val result = retrieveFolderOperations.hasMoreMessages(folderId) + + assertThat(result).isEqualTo(MoreMessages.TRUE) + } } -- GitLab From be5867de83cb8b86ecdc8a0c4794f304f53b89fd Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 30 Aug 2022 12:39:57 +0200 Subject: [PATCH 38/85] Add `MessageStore.getThreadedMessages()` This is more or less a copy of `EmailProvider.getThreadedMessages()`. --- .../k9/mailstore/DatabasePreviewType.java | 2 + .../com/fsck/k9/mailstore/MessageMapper.kt | 28 ++ .../com/fsck/k9/mailstore/MessageStore.kt | 10 + .../k9/storage/messages/K9MessageStore.kt | 11 + .../messages/RetrieveMessageListOperations.kt | 143 ++++++++ .../messages/MessageDatabaseHelpers.kt | 2 +- .../RetrieveMessageListOperationsTest.kt | 333 ++++++++++++++++++ 7 files changed, 528 insertions(+), 1 deletion(-) create mode 100644 app/core/src/main/java/com/fsck/k9/mailstore/MessageMapper.kt create mode 100644 app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt create mode 100644 app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/DatabasePreviewType.java b/app/core/src/main/java/com/fsck/k9/mailstore/DatabasePreviewType.java index 40a262caae..e0627bfb8b 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/DatabasePreviewType.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/DatabasePreviewType.java @@ -2,6 +2,7 @@ package com.fsck.k9.mailstore; import com.fsck.k9.message.extractors.PreviewResult.PreviewType; +import org.jetbrains.annotations.NotNull; public enum DatabasePreviewType { @@ -20,6 +21,7 @@ public enum DatabasePreviewType { this.previewType = previewType; } + @NotNull public static DatabasePreviewType fromDatabaseValue(String databaseValue) { for (DatabasePreviewType databasePreviewType : values()) { if (databasePreviewType.getDatabaseValue().equals(databaseValue)) { diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageMapper.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageMapper.kt new file mode 100644 index 0000000000..a8c15d96c3 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageMapper.kt @@ -0,0 +1,28 @@ +package com.fsck.k9.mailstore + +import com.fsck.k9.mail.Address +import com.fsck.k9.message.extractors.PreviewResult + +fun interface MessageMapper { + fun map(message: MessageDetailsAccessor): T +} + +interface MessageDetailsAccessor { + val id: Long + val messageServerId: String + val folderId: Long + val fromAddresses: List
+ val toAddresses: List
+ val ccAddresses: List
+ val messageDate: Long + val internalDate: Long + val subject: String? + val preview: PreviewResult + val isRead: Boolean + val isStarred: Boolean + val isAnswered: Boolean + val isForwarded: Boolean + val hasAttachments: Boolean + val threadRoot: Long + val threadCount: Int +} diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt index 8d55d9149d..10aa047398 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt @@ -120,6 +120,16 @@ interface MessageStore { */ fun getAllMessagesAndEffectiveDates(folderId: Long): Map + /** + * Retrieve threaded list of messages. + */ + fun getThreadedMessages( + selection: String, + selectionArgs: Array, + sortOrder: String, + messageMapper: MessageMapper + ): List + /** * Retrieve the date of the oldest message in the given folder. */ diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt index ceb7a12578..0601247ef4 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt @@ -9,6 +9,7 @@ import com.fsck.k9.mailstore.CreateFolderInfo import com.fsck.k9.mailstore.FolderDetails import com.fsck.k9.mailstore.FolderMapper import com.fsck.k9.mailstore.LockableDatabase +import com.fsck.k9.mailstore.MessageMapper import com.fsck.k9.mailstore.MessageStore import com.fsck.k9.mailstore.MoreMessages import com.fsck.k9.mailstore.SaveMessageData @@ -35,6 +36,7 @@ class K9MessageStore( private val flagMessageOperations = FlagMessageOperations(database) private val updateMessageOperations = UpdateMessageOperations(database) private val retrieveMessageOperations = RetrieveMessageOperations(database) + private val retrieveMessageListOperations = RetrieveMessageListOperations(database) private val deleteMessageOperations = DeleteMessageOperations(database, attachmentFileManager) private val createFolderOperations = CreateFolderOperations(database) private val retrieveFolderOperations = RetrieveFolderOperations(database) @@ -100,6 +102,15 @@ class K9MessageStore( return retrieveMessageOperations.getAllMessagesAndEffectiveDates(folderId) } + override fun getThreadedMessages( + selection: String, + selectionArgs: Array, + sortOrder: String, + messageMapper: MessageMapper + ): List { + return retrieveMessageListOperations.getThreadedMessages(selection, selectionArgs, sortOrder, messageMapper) + } + override fun getOldestMessageDate(folderId: Long): Date? { return retrieveMessageOperations.getOldestMessageDate(folderId) } diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt new file mode 100644 index 0000000000..8a36393669 --- /dev/null +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt @@ -0,0 +1,143 @@ +package com.fsck.k9.storage.messages + +import android.database.Cursor +import com.fsck.k9.helper.map +import com.fsck.k9.mail.Address +import com.fsck.k9.mailstore.DatabasePreviewType +import com.fsck.k9.mailstore.LockableDatabase +import com.fsck.k9.mailstore.MessageDetailsAccessor +import com.fsck.k9.mailstore.MessageMapper +import com.fsck.k9.message.extractors.PreviewResult +import com.fsck.k9.search.SqlQueryBuilder + +internal class RetrieveMessageListOperations(private val lockableDatabase: LockableDatabase) { + + fun getThreadedMessages( + selection: String, + selectionArgs: Array, + sortOrder: String, + mapper: MessageMapper + ): List { + val orderBy = SqlQueryBuilder.addPrefixToSelection(AGGREGATED_MESSAGES_COLUMNS, "aggregated.", sortOrder) + + return lockableDatabase.execute(false) { database -> + database.rawQuery( + """ + SELECT + messages.id AS id, + uid, + folder_id, + sender_list, + to_list, + cc_list, + aggregated.date AS date, + aggregated.internal_date AS internal_date, + subject, + preview_type, + preview, + aggregated.read AS read, + aggregated.flagged AS flagged, + aggregated.answered AS answered, + aggregated.forwarded AS forwarded, + aggregated.attachment_count AS attachment_count, + root, + aggregated.thread_count AS thread_count + FROM ( + SELECT + threads.root AS thread_root, + MAX(date) AS date, + MAX(internal_date) AS internal_date, + MIN(read) AS read, + MAX(flagged) AS flagged, + MIN(answered) AS answered, + MIN(forwarded) AS forwarded, + SUM(attachment_count) AS attachment_count, + COUNT(threads.root) AS thread_count + FROM messages + JOIN threads ON (threads.message_id = messages.id) + JOIN folders ON (folders.id = messages.folder_id) + WHERE + threads.root IN ( + SELECT threads.root + FROM messages + JOIN threads ON (threads.message_id = messages.id) + WHERE messages.empty = 0 AND messages.deleted = 0 + ) + AND ($selection) + AND messages.empty = 0 AND messages.deleted = 0 + GROUP BY threads.root + ) aggregated + JOIN threads ON (threads.root = aggregated.thread_root) + JOIN messages ON ( + messages.id = threads.message_id + AND messages.empty = 0 AND messages.deleted = 0 + AND messages.date = aggregated.date + ) + JOIN folders ON (folders.id = messages.folder_id) + GROUP BY threads.root + ORDER BY $orderBy + """.trimIndent(), + selectionArgs, + ).use { cursor -> + val cursorMessageAccessor = CursorMessageAccessor(cursor) + cursor.map { + mapper.map(cursorMessageAccessor) + } + } + } + } +} + +private class CursorMessageAccessor(val cursor: Cursor) : MessageDetailsAccessor { + override val id: Long + get() = cursor.getLong(0) + override val messageServerId: String + get() = cursor.getString(1) + override val folderId: Long + get() = cursor.getLong(2) + override val fromAddresses: List
+ get() = Address.unpack(cursor.getString(3)).toList() + override val toAddresses: List
+ get() = Address.unpack(cursor.getString(4)).toList() + override val ccAddresses: List
+ get() = Address.unpack(cursor.getString(5)).toList() + override val messageDate: Long + get() = cursor.getLong(6) + override val internalDate: Long + get() = cursor.getLong(7) + override val subject: String? + get() = cursor.getString(8) + override val preview: PreviewResult + get() { + return when (DatabasePreviewType.fromDatabaseValue(cursor.getString(9))) { + DatabasePreviewType.NONE -> PreviewResult.none() + DatabasePreviewType.TEXT -> PreviewResult.text(cursor.getString(10)) + DatabasePreviewType.ENCRYPTED -> PreviewResult.encrypted() + DatabasePreviewType.ERROR -> PreviewResult.error() + } + } + override val isRead: Boolean + get() = cursor.getInt(11) == 1 + override val isStarred: Boolean + get() = cursor.getInt(12) == 1 + override val isAnswered: Boolean + get() = cursor.getInt(13) == 1 + override val isForwarded: Boolean + get() = cursor.getInt(14) == 1 + override val hasAttachments: Boolean + get() = cursor.getInt(15) > 0 + override val threadRoot: Long + get() = cursor.getLong(16) + override val threadCount: Int + get() = cursor.getInt(17) +} + +private val AGGREGATED_MESSAGES_COLUMNS = arrayOf( + "date", + "internal_date", + "attachment_count", + "read", + "flagged", + "answered", + "forwarded" +) diff --git a/app/storage/src/test/java/com/fsck/k9/storage/messages/MessageDatabaseHelpers.kt b/app/storage/src/test/java/com/fsck/k9/storage/messages/MessageDatabaseHelpers.kt index bd71c62791..0c9bbf74a3 100644 --- a/app/storage/src/test/java/com/fsck/k9/storage/messages/MessageDatabaseHelpers.kt +++ b/app/storage/src/test/java/com/fsck/k9/storage/messages/MessageDatabaseHelpers.kt @@ -38,8 +38,8 @@ fun createDatabase(): SQLiteDatabase { } fun SQLiteDatabase.createMessage( - deleted: Boolean = false, folderId: Long, + deleted: Boolean = false, uid: String? = null, subject: String = "", date: Long = 0L, diff --git a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt new file mode 100644 index 0000000000..3d280fb475 --- /dev/null +++ b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt @@ -0,0 +1,333 @@ +package com.fsck.k9.storage.messages + +import com.fsck.k9.mail.Address +import com.fsck.k9.mailstore.DatabasePreviewType +import com.fsck.k9.mailstore.MessageMapper +import com.fsck.k9.message.extractors.PreviewResult.PreviewType +import com.fsck.k9.storage.RobolectricTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class RetrieveMessageListOperationsTest : RobolectricTest() { + private val sqliteDatabase = createDatabase() + private val lockableDatabase = createLockableDatabaseMock(sqliteDatabase) + private val retrieveMessageListOperations = RetrieveMessageListOperations(lockableDatabase) + + @Test + fun `getThreadedMessages() on empty folder`() { + val folderId = sqliteDatabase.createFolder() + + val result = getThreadedMessagesFromFolder(folderId) { "unexpected" } + + assertThat(result).isEmpty() + } + + @Test + fun `getThreadedMessages() with only a deleted message`() { + val folderId = sqliteDatabase.createFolder() + val messageId = sqliteDatabase.createMessage(folderId, uid = "uid1", deleted = true) + sqliteDatabase.createThread(messageId) + + val result = getThreadedMessagesFromFolder(folderId) { "unexpected" } + + assertThat(result).isEmpty() + } + + @Test + fun `getThreadedMessages() with single message`() { + val folderId = sqliteDatabase.createFolder() + val messageId = sqliteDatabase.createMessage( + folderId, + uid = "uid1", + subject = "subject", + date = 123L, + senderList = Address.pack(Address.parse("from@domain.example")), + toList = Address.pack(Address.parse("to@domain.example")), + ccList = Address.pack(Address.parse("cc1@domain.example, cc2@domain.example")), + attachmentCount = 1, + internalDate = 456L, + previewType = DatabasePreviewType.TEXT, + preview = "preview", + read = true, + flagged = true, + answered = true, + forwarded = true + ) + val threadId = sqliteDatabase.createThread(messageId) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.id).isEqualTo(messageId) + assertThat(message.messageServerId).isEqualTo("uid1") + assertThat(message.folderId).isEqualTo(folderId) + assertThat(message.fromAddresses).containsExactly(Address("from@domain.example")) + assertThat(message.toAddresses).containsExactly(Address("to@domain.example")) + assertThat(message.ccAddresses).containsExactly(Address("cc1@domain.example"), Address("cc2@domain.example")) + assertThat(message.messageDate).isEqualTo(123L) + assertThat(message.internalDate).isEqualTo(456L) + assertThat(message.subject).isEqualTo("subject") + assertThat(message.preview.previewType).isEqualTo(PreviewType.TEXT) + assertThat(message.preview.previewText).isEqualTo("preview") + assertThat(message.isRead).isTrue() + assertThat(message.isStarred).isTrue() + assertThat(message.isAnswered).isTrue() + assertThat(message.isForwarded).isTrue() + assertThat(message.hasAttachments).isTrue() + assertThat(message.threadRoot).isEqualTo(threadId) + assertThat(message.threadCount).isEqualTo(1) + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() with thread containing an empty message`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, empty = true) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2") + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.id).isEqualTo(messageId2) + assertThat(message.messageServerId).isEqualTo("uid2") + assertThat(message.threadRoot).isEqualTo(threadId1) + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return latest message in thread`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage( + folderId, + uid = "uid1", + date = 1000L, + internalDate = 1001L + ) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage( + folderId, + uid = "uid2", + date = 2000L, + internalDate = 2001L + ) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.id).isEqualTo(messageId2) + assertThat(message.messageDate).isEqualTo(2000L) + assertThat(message.internalDate).isEqualTo(2001L) + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return 'unread' when at least one message in thread is marked as unread`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", read = true) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", read = false) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.isRead).isFalse() + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return 'read' when all messages in thread are marked as read`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", read = true) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", read = true) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.isRead).isTrue() + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return 'starred' when at least one message in thread is marked as starred`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", flagged = false) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", flagged = true) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.isStarred).isTrue() + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return 'not starred' when all messages in thread are not marked as starred`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", flagged = false) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", flagged = false) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.isStarred).isFalse() + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return 'not answered' when not all messages in thread are marked as answered`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", answered = false) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", answered = true) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.isAnswered).isFalse() + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return 'answered' when all messages in thread are marked as answered`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", answered = true) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", answered = true) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.isAnswered).isTrue() + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return 'not forwarded' when not all messages in thread are marked as forwarded`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", forwarded = true) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", forwarded = false) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.isForwarded).isFalse() + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return 'forwarded' when all messages in thread are marked as forwarded`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", forwarded = true) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", forwarded = true) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.isForwarded).isTrue() + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return 'has attachment' when at least one message in thread contains an attachment`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", attachmentCount = 1) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", attachmentCount = 0) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.hasAttachments).isTrue() + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should return 'has no attachment' when no message in thread contains an attachment`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", attachmentCount = 0) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", attachmentCount = 0) + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.hasAttachments).isFalse() + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() with 3 messages in thread`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1") + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2") + sqliteDatabase.createThread(messageId2, root = threadId1) + val messageId3 = sqliteDatabase.createMessage(folderId, uid = "uid3") + sqliteDatabase.createThread(messageId3, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.threadCount).isEqualTo(3) + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getThreadedMessages() should not include empty messages in thread count`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", empty = true) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2") + sqliteDatabase.createThread(messageId2, root = threadId1) + val messageId3 = sqliteDatabase.createMessage(folderId, uid = "uid3") + sqliteDatabase.createThread(messageId3, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> + assertThat(message.threadCount).isEqualTo(2) + "OK" + } + + assertThat(result).containsExactly("OK") + } + + private fun getThreadedMessagesFromFolder(folderId: Long, mapper: MessageMapper): List { + return retrieveMessageListOperations.getThreadedMessages( + selection = "folder_id = ?", + selectionArgs = arrayOf(folderId.toString()), + sortOrder = "date DESC, id DESC", + mapper + ) + } +} -- GitLab From 9b90d180519732725e4b3f33975bbfd19c500436 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 30 Aug 2022 13:43:08 +0200 Subject: [PATCH 39/85] Add `MessageStore.getMessages()` This is more or less a copy of `EmailProvider.getMessages()`. --- .../com/fsck/k9/mailstore/MessageStore.kt | 10 ++ .../k9/storage/messages/K9MessageStore.kt | 9 ++ .../messages/RetrieveMessageListOperations.kt | 51 +++++++- .../RetrieveMessageListOperationsTest.kt | 123 ++++++++++++++++++ 4 files changed, 190 insertions(+), 3 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt index 10aa047398..bde9c3ecbd 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt @@ -120,6 +120,16 @@ interface MessageStore { */ fun getAllMessagesAndEffectiveDates(folderId: Long): Map + /** + * Retrieve list of messages. + */ + fun getMessages( + selection: String, + selectionArgs: Array, + sortOrder: String, + messageMapper: MessageMapper + ): List + /** * Retrieve threaded list of messages. */ diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt index 0601247ef4..b81fb99b6f 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt @@ -102,6 +102,15 @@ class K9MessageStore( return retrieveMessageOperations.getAllMessagesAndEffectiveDates(folderId) } + override fun getMessages( + selection: String, + selectionArgs: Array, + sortOrder: String, + messageMapper: MessageMapper + ): List { + return retrieveMessageListOperations.getMessages(selection, selectionArgs, sortOrder, messageMapper) + } + override fun getThreadedMessages( selection: String, selectionArgs: Array, diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt index 8a36393669..989c49198a 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt @@ -12,6 +12,51 @@ import com.fsck.k9.search.SqlQueryBuilder internal class RetrieveMessageListOperations(private val lockableDatabase: LockableDatabase) { + fun getMessages( + selection: String, + selectionArgs: Array, + sortOrder: String, + mapper: MessageMapper + ): List { + return lockableDatabase.execute(false) { database -> + database.rawQuery( + """ + SELECT + messages.id AS id, + uid, + folder_id, + sender_list, + to_list, + cc_list, + date, + internal_date, + subject, + preview_type, + preview, + read, + flagged, + answered, + forwarded, + attachment_count, + root + FROM messages + JOIN threads ON (threads.message_id = messages.id) + LEFT JOIN FOLDERS ON (folders.id = messages.folder_id) + WHERE + ($selection) + AND empty = 0 AND deleted = 0 + ORDER BY $sortOrder + """.trimIndent(), + selectionArgs, + ).use { cursor -> + val cursorMessageAccessor = CursorMessageAccessor(cursor, includesThreadCount = false) + cursor.map { + mapper.map(cursorMessageAccessor) + } + } + } + } + fun getThreadedMessages( selection: String, selectionArgs: Array, @@ -79,7 +124,7 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka """.trimIndent(), selectionArgs, ).use { cursor -> - val cursorMessageAccessor = CursorMessageAccessor(cursor) + val cursorMessageAccessor = CursorMessageAccessor(cursor, includesThreadCount = true) cursor.map { mapper.map(cursorMessageAccessor) } @@ -88,7 +133,7 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka } } -private class CursorMessageAccessor(val cursor: Cursor) : MessageDetailsAccessor { +private class CursorMessageAccessor(val cursor: Cursor, val includesThreadCount: Boolean) : MessageDetailsAccessor { override val id: Long get() = cursor.getLong(0) override val messageServerId: String @@ -129,7 +174,7 @@ private class CursorMessageAccessor(val cursor: Cursor) : MessageDetailsAccessor override val threadRoot: Long get() = cursor.getLong(16) override val threadCount: Int - get() = cursor.getInt(17) + get() = if (includesThreadCount) cursor.getInt(17) else 0 } private val AGGREGATED_MESSAGES_COLUMNS = arrayOf( diff --git a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt index 3d280fb475..b184f950aa 100644 --- a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt +++ b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt @@ -13,6 +13,120 @@ class RetrieveMessageListOperationsTest : RobolectricTest() { private val lockableDatabase = createLockableDatabaseMock(sqliteDatabase) private val retrieveMessageListOperations = RetrieveMessageListOperations(lockableDatabase) + @Test + fun `getMessages() on empty folder`() { + val folderId = sqliteDatabase.createFolder() + + val result = getMessagesFromFolder(folderId) { "unexpected" } + + assertThat(result).isEmpty() + } + + @Test + fun `getMessages() with only a deleted message`() { + val folderId = sqliteDatabase.createFolder() + val messageId = sqliteDatabase.createMessage(folderId, uid = "uid1", deleted = true) + sqliteDatabase.createThread(messageId) + + val result = getMessagesFromFolder(folderId) { "unexpected" } + + assertThat(result).isEmpty() + } + + @Test + fun `getMessages() with single message`() { + val folderId = sqliteDatabase.createFolder() + val messageId = sqliteDatabase.createMessage( + folderId, + uid = "uid1", + subject = "subject", + date = 123L, + senderList = Address.pack(Address.parse("from@domain.example")), + toList = Address.pack(Address.parse("to@domain.example")), + ccList = Address.pack(Address.parse("cc1@domain.example, cc2@domain.example")), + attachmentCount = 1, + internalDate = 456L, + previewType = DatabasePreviewType.TEXT, + preview = "preview", + read = true, + flagged = true, + answered = true, + forwarded = true + ) + val threadId = sqliteDatabase.createThread(messageId) + + val result = getMessagesFromFolder(folderId) { message -> + assertThat(message.id).isEqualTo(messageId) + assertThat(message.messageServerId).isEqualTo("uid1") + assertThat(message.folderId).isEqualTo(folderId) + assertThat(message.fromAddresses).containsExactly(Address("from@domain.example")) + assertThat(message.toAddresses).containsExactly(Address("to@domain.example")) + assertThat(message.ccAddresses).containsExactly(Address("cc1@domain.example"), Address("cc2@domain.example")) + assertThat(message.messageDate).isEqualTo(123L) + assertThat(message.internalDate).isEqualTo(456L) + assertThat(message.subject).isEqualTo("subject") + assertThat(message.preview.previewType).isEqualTo(PreviewType.TEXT) + assertThat(message.preview.previewText).isEqualTo("preview") + assertThat(message.isRead).isTrue() + assertThat(message.isStarred).isTrue() + assertThat(message.isAnswered).isTrue() + assertThat(message.isForwarded).isTrue() + assertThat(message.hasAttachments).isTrue() + assertThat(message.threadRoot).isEqualTo(threadId) + assertThat(message.threadCount).isEqualTo(0) + "OK" + } + + assertThat(result).containsExactly("OK") + } + + @Test + fun `getMessages() with folder containing an empty message`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, empty = true) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2") + sqliteDatabase.createThread(messageId2, root = threadId1) + + val result = getThreadedMessagesFromFolder(folderId) { message -> message.id } + + assertThat(result).containsExactly(messageId2) + } + + @Test + fun `getMessages() with folder containing a deleted message`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, deleted = true) + sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2") + sqliteDatabase.createThread(messageId2) + + val result = getThreadedMessagesFromFolder(folderId) { message -> message.id } + + assertThat(result).containsExactly(messageId2) + } + + @Test + fun `getMessages() selecting only unread messages`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1", read = false) + sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2", read = true) + sqliteDatabase.createThread(messageId2) + val messageId3 = sqliteDatabase.createMessage(folderId, uid = "uid3", read = false) + sqliteDatabase.createThread(messageId3) + + val result = retrieveMessageListOperations.getThreadedMessages( + selection = "folder_id = ? AND read = 0", + selectionArgs = arrayOf(folderId.toString()), + sortOrder = "date DESC, id DESC" + ) { message -> + message.id + } + + assertThat(result).containsExactly(messageId1, messageId3) + } + @Test fun `getThreadedMessages() on empty folder`() { val folderId = sqliteDatabase.createFolder() @@ -322,6 +436,15 @@ class RetrieveMessageListOperationsTest : RobolectricTest() { assertThat(result).containsExactly("OK") } + private fun getMessagesFromFolder(folderId: Long, mapper: MessageMapper): List { + return retrieveMessageListOperations.getMessages( + selection = "folder_id = ?", + selectionArgs = arrayOf(folderId.toString()), + sortOrder = "date DESC, id DESC", + mapper + ) + } + private fun getThreadedMessagesFromFolder(folderId: Long, mapper: MessageMapper): List { return retrieveMessageListOperations.getThreadedMessages( selection = "folder_id = ?", -- GitLab From e14ce585a5af17685c17a216bd272f70b0ac51e5 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 30 Aug 2022 14:49:30 +0200 Subject: [PATCH 40/85] Add `MessageStore.getThread()` This is more or less a copy of `EmailProvider.getThread()`. --- .../com/fsck/k9/mailstore/MessageStore.kt | 5 +++ .../k9/storage/messages/K9MessageStore.kt | 4 ++ .../messages/RetrieveMessageListOperations.kt | 40 +++++++++++++++++++ .../RetrieveMessageListOperationsTest.kt | 30 ++++++++++++++ 4 files changed, 79 insertions(+) diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt index bde9c3ecbd..91d8f8bf70 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt @@ -140,6 +140,11 @@ interface MessageStore { messageMapper: MessageMapper ): List + /** + * Retrieve list of messages in a thread. + */ + fun getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper): List + /** * Retrieve the date of the oldest message in the given folder. */ diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt index b81fb99b6f..239f5aab3f 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt @@ -120,6 +120,10 @@ class K9MessageStore( return retrieveMessageListOperations.getThreadedMessages(selection, selectionArgs, sortOrder, messageMapper) } + override fun getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper): List { + return retrieveMessageListOperations.getThread(threadId, sortOrder, messageMapper) + } + override fun getOldestMessageDate(folderId: Long): Date? { return retrieveMessageOperations.getOldestMessageDate(folderId) } diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt index 989c49198a..896d2ee5df 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt @@ -131,6 +131,46 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka } } } + + fun getThread(threadId: Long, sortOrder: String, mapper: MessageMapper): List { + return lockableDatabase.execute(false) { database -> + database.rawQuery( + """ + SELECT + messages.id AS id, + uid, + folder_id, + sender_list, + to_list, + cc_list, + date, + internal_date, + subject, + preview_type, + preview, + read, + flagged, + answered, + forwarded, + attachment_count, + root + FROM threads + JOIN messages ON (messages.id = threads.message_id) + LEFT JOIN FOLDERS ON (folders.id = messages.folder_id) + WHERE + root = ? + AND empty = 0 AND deleted = 0 + ORDER BY $sortOrder + """.trimIndent(), + arrayOf(threadId.toString()), + ).use { cursor -> + val cursorMessageAccessor = CursorMessageAccessor(cursor, includesThreadCount = false) + cursor.map { + mapper.map(cursorMessageAccessor) + } + } + } + } } private class CursorMessageAccessor(val cursor: Cursor, val includesThreadCount: Boolean) : MessageDetailsAccessor { diff --git a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt index b184f950aa..8e8db34f39 100644 --- a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt +++ b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt @@ -436,6 +436,36 @@ class RetrieveMessageListOperationsTest : RobolectricTest() { assertThat(result).containsExactly("OK") } + @Test + fun `getThread() with empty message as thread root`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, empty = true) + val threadId1 = sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2") + sqliteDatabase.createThread(messageId2, root = threadId1) + val messageId3 = sqliteDatabase.createMessage(folderId, uid = "uid3") + sqliteDatabase.createThread(messageId3, root = threadId1) + + val result = retrieveMessageListOperations.getThread(threadId = threadId1, sortOrder = "date DESC") { it.id } + + assertThat(result).containsExactly(messageId2, messageId3) + } + + @Test + fun `getThread() should only return messages in thread`() { + val folderId = sqliteDatabase.createFolder() + val messageId1 = sqliteDatabase.createMessage(folderId, uid = "uid1") + sqliteDatabase.createThread(messageId1) + val messageId2 = sqliteDatabase.createMessage(folderId, uid = "uid2") + val threadId2 = sqliteDatabase.createThread(messageId2) + val messageId3 = sqliteDatabase.createMessage(folderId, uid = "uid3") + sqliteDatabase.createThread(messageId3, root = threadId2) + + val result = retrieveMessageListOperations.getThread(threadId = threadId2, sortOrder = "date DESC") { it.id } + + assertThat(result).containsExactly(messageId2, messageId3) + } + private fun getMessagesFromFolder(folderId: Long, mapper: MessageMapper): List { return retrieveMessageListOperations.getMessages( selection = "folder_id = ?", -- GitLab From 18b177e18efd3ebb05c3bb624397c1a7d3d29f59 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 30 Aug 2022 16:32:41 +0200 Subject: [PATCH 41/85] Remove `MessageListItem.position` --- .../java/com/fsck/k9/fragment/MessageListAdapter.kt | 13 ++++++++++--- .../java/com/fsck/k9/fragment/MessageViewHolder.kt | 2 +- .../fsck/k9/ui/messagelist/MessageListExtractor.kt | 2 -- .../com/fsck/k9/ui/messagelist/MessageListItem.kt | 1 - .../com/fsck/k9/fragment/MessageListAdapterTest.kt | 2 -- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt index 468ac24c52..e06af5b0ec 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt @@ -55,9 +55,12 @@ class MessageListAdapter internal constructor( var messages: List = emptyList() set(value) { field = value + messagesMap = value.associateBy { it.uniqueId } notifyDataSetChanged() } + private var messagesMap = emptyMap() + var activeMessage: MessageReference? = null var selected: Set = emptySet() @@ -71,14 +74,14 @@ class MessageListAdapter internal constructor( private val flagClickListener = OnClickListener { view: View -> val messageViewHolder = view.tag as MessageViewHolder - val messageListItem = getItem(messageViewHolder.position) + val messageListItem = getItemById(messageViewHolder.uniqueId) listItemListener.onToggleMessageFlag(messageListItem) } private val contactPictureClickListener = OnClickListener { view: View -> val parentView = view.parent.parent as View val messageViewHolder = parentView.tag as MessageViewHolder - val messageListItem = getItem(messageViewHolder.position) + val messageListItem = getItemById(messageViewHolder.uniqueId) listItemListener.onToggleMessageSelection(messageListItem) } @@ -96,6 +99,10 @@ class MessageListAdapter internal constructor( override fun getItem(position: Int): MessageListItem = messages[position] + private fun getItemById(uniqueId: Long): MessageListItem { + return messagesMap[uniqueId]!! + } + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { val message = getItem(position) val view: View = convertView ?: newView(parent) @@ -163,7 +170,7 @@ class MessageListAdapter internal constructor( if (appearance.stars) { holder.flagged.isChecked = isStarred } - holder.position = position + holder.uniqueId = uniqueId if (appearance.showContactPicture && holder.contactPicture.isVisible) { setContactPicture(holder.contactPicture, displayAddress) } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt index 9afd334390..d6487ab9b5 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt @@ -7,7 +7,7 @@ import android.widget.TextView import com.fsck.k9.ui.R class MessageViewHolder(view: View) { - var position = -1 + var uniqueId: Long = -1L val selected: View = view.findViewById(R.id.selected) val contactPicture: ImageView = view.findViewById(R.id.contact_picture) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt index 0f241475af..ec4d46865e 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt @@ -22,7 +22,6 @@ class MessageListExtractor( uniqueIdColumn: Int, threadCountIncluded: Boolean ): MessageListItem { - val position = cursor.position val accountUuid = cursor.getString(MLFProjectionInfo.ACCOUNT_UUID_COLUMN) val account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found") @@ -60,7 +59,6 @@ class MessageListExtractor( } return MessageListItem( - position, account, subject, threadCount, diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt index 3797bea8a1..ea2df9a054 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt @@ -4,7 +4,6 @@ import com.fsck.k9.Account import com.fsck.k9.mail.Address data class MessageListItem( - val position: Int, val account: Account, val subject: String?, val threadCount: Int, diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt index 029b1421e2..cdbacdb2ce 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt @@ -469,7 +469,6 @@ class MessageListAdapterTest : RobolectricTest() { } fun createMessageListItem( - position: Int = 0, account: Account = Account(SOME_ACCOUNT_UUID), subject: String? = "irrelevant", threadCount: Int = 0, @@ -492,7 +491,6 @@ class MessageListAdapterTest : RobolectricTest() { threadRoot: Long = 0L ): MessageListItem { return MessageListItem( - position, account, subject, threadCount, -- GitLab From e9b91f3654c2ee0889b4c22aa9972a2b6b1df764 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 30 Aug 2022 18:26:27 +0200 Subject: [PATCH 42/85] Add `MessageListRepository` Remove the "message list changed" notification mechanism provided by `EmailProvider` and use a simple callback mechanism instead. --- .../com/fsck/k9/cache/EmailProviderCache.java | 9 ++-- .../java/com/fsck/k9/mailstore/KoinModule.kt | 1 + .../com/fsck/k9/mailstore/LocalStore.java | 8 +--- .../k9/mailstore/MessageListRepository.kt | 28 +++++++++++ .../com/fsck/k9/provider/EmailProvider.java | 2 +- .../k9/mailstore/MessageListRepositoryTest.kt | 47 +++++++++++++++++++ .../fsck/k9/ui/account/AccountsViewModel.kt | 23 ++++----- .../java/com/fsck/k9/ui/account/KoinModule.kt | 4 +- .../com/fsck/k9/ui/messagelist/KoinModule.kt | 4 +- .../k9/ui/messagelist/MessageListLiveData.kt | 41 ++++++---------- .../messagelist/MessageListLiveDataFactory.kt | 6 +-- 11 files changed, 114 insertions(+), 59 deletions(-) create mode 100644 app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt create mode 100644 app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt diff --git a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.java b/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.java index cc12fde40c..66e66d2cf3 100644 --- a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.java +++ b/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.java @@ -5,10 +5,10 @@ import java.util.List; import java.util.Map; import android.content.Context; -import android.net.Uri; +import com.fsck.k9.DI; import com.fsck.k9.mailstore.LocalMessage; -import com.fsck.k9.provider.EmailProvider; +import com.fsck.k9.mailstore.MessageListRepository; /** * Cache to bridge the time needed to write (user-initiated) changes to the database. @@ -149,8 +149,7 @@ public class EmailProviderCache { } private void notifyChange() { - Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + mAccountUuid + - "/messages"); - sContext.getContentResolver().notifyChange(uri, null); + MessageListRepository messageListRepository = DI.get(MessageListRepository.class); + messageListRepository.notifyMessageListChanged(mAccountUuid); } } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt index 5f657ecaa4..e2465297af 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt @@ -34,4 +34,5 @@ val mailStoreModule = module { attachmentCounter = get() ) } + single { MessageListRepository() } } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java index 4e33ebfea2..a502008e87 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -23,7 +23,6 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; import androidx.annotation.Nullable; import android.text.TextUtils; @@ -51,7 +50,6 @@ import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.SchemaDefinition; import com.fsck.k9.mailstore.StorageManager.InternalStorageProvider; import com.fsck.k9.message.extractors.AttachmentInfoExtractor; -import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.MessageColumns; import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchSpecification.Attribute; @@ -166,7 +164,6 @@ public class LocalStore { private static final int THREAD_FLAG_UPDATE_BATCH_SIZE = 500; private final Context context; - private final ContentResolver contentResolver; private final PendingCommandSerializer pendingCommandSerializer; private final AttachmentInfoExtractor attachmentInfoExtractor; @@ -184,7 +181,6 @@ public class LocalStore { */ private LocalStore(final Account account, final Context context) throws MessagingException { this.context = context; - this.contentResolver = context.getContentResolver(); pendingCommandSerializer = PendingCommandSerializer.getInstance(); attachmentInfoExtractor = DI.get(AttachmentInfoExtractor.class); @@ -726,8 +722,8 @@ public class LocalStore { } public void notifyChange() { - Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + account.getUuid() + "/messages"); - contentResolver.notifyChange(uri, null); + MessageListRepository messageListRepository = DI.get(MessageListRepository.class); + messageListRepository.notifyMessageListChanged(account.getUuid()); } /** diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt new file mode 100644 index 0000000000..d852bd9451 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt @@ -0,0 +1,28 @@ +package com.fsck.k9.mailstore + +import java.util.concurrent.CopyOnWriteArraySet + +class MessageListRepository { + private val listeners = CopyOnWriteArraySet>() + + fun addListener(accountUuid: String, listener: MessageListChangedListener) { + listeners.add(accountUuid to listener) + } + + fun removeListener(listener: MessageListChangedListener) { + val entries = listeners.filter { it.second == listener }.toSet() + listeners.removeAll(entries) + } + + fun notifyMessageListChanged(accountUuid: String) { + for (listener in listeners) { + if (listener.first == accountUuid) { + listener.second.onMessageListChanged() + } + } + } +} + +fun interface MessageListChangedListener { + fun onMessageListChanged() +} diff --git a/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java b/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java index da70da7827..69508571ef 100644 --- a/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java +++ b/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java @@ -46,7 +46,7 @@ public class EmailProvider extends ContentProvider { public static String AUTHORITY; public static Uri CONTENT_URI; - public static Uri getNotificationUri(String accountUuid) { + private static Uri getNotificationUri(String accountUuid) { return Uri.withAppendedPath(CONTENT_URI, "account/" + accountUuid + "/messages"); } diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt new file mode 100644 index 0000000000..e32fbe8678 --- /dev/null +++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt @@ -0,0 +1,47 @@ +package com.fsck.k9.mailstore + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +private const val ACCOUNT_UUID = "00000000-0000-4000-0000-000000000000" + +class MessageListRepositoryTest { + private val messageListRepository = MessageListRepository() + + @Test + fun `adding and removing listener`() { + var messageListChanged = 0 + val listener = MessageListChangedListener { + messageListChanged++ + } + messageListRepository.addListener(ACCOUNT_UUID, listener) + + messageListRepository.notifyMessageListChanged(ACCOUNT_UUID) + + assertThat(messageListChanged).isEqualTo(1) + + messageListRepository.removeListener(listener) + + messageListRepository.notifyMessageListChanged(ACCOUNT_UUID) + + assertThat(messageListChanged).isEqualTo(1) + } + + @Test + fun `only notify listener when account UUID matches`() { + var messageListChanged = 0 + val listener = MessageListChangedListener { + messageListChanged++ + } + messageListRepository.addListener(ACCOUNT_UUID, listener) + + messageListRepository.notifyMessageListChanged("otherAccountUuid") + + assertThat(messageListChanged).isEqualTo(0) + } + + @Test + fun `notifyMessageListChanged() without any listeners should not throw`() { + messageListRepository.notifyMessageListChanged(ACCOUNT_UUID) + } +} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/account/AccountsViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/account/AccountsViewModel.kt index cb25a0dc8c..488e5c90f6 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/account/AccountsViewModel.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/account/AccountsViewModel.kt @@ -1,17 +1,14 @@ package com.fsck.k9.ui.account -import android.content.ContentResolver -import android.database.ContentObserver -import android.os.Handler -import android.os.Looper import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import com.fsck.k9.Account import com.fsck.k9.controller.MessageCounts import com.fsck.k9.controller.MessageCountsProvider +import com.fsck.k9.mailstore.MessageListChangedListener +import com.fsck.k9.mailstore.MessageListRepository import com.fsck.k9.preferences.AccountManager -import com.fsck.k9.provider.EmailProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose @@ -26,7 +23,7 @@ import kotlinx.coroutines.launch class AccountsViewModel( accountManager: AccountManager, private val messageCountsProvider: MessageCountsProvider, - private val contentResolver: ContentResolver + private val messageListRepository: MessageListRepository ) : ViewModel() { private val displayAccountFlow: Flow> = accountManager.getAccountsFlow() .flatMapLatest { accounts -> @@ -47,21 +44,17 @@ class AccountsViewModel( private fun getMessageCountsFlow(account: Account): Flow { return callbackFlow { - val notificationUri = EmailProvider.getNotificationUri(account.uuid) - send(messageCountsProvider.getMessageCounts(account)) - val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { - override fun onChange(selfChange: Boolean) { - launch { - send(messageCountsProvider.getMessageCounts(account)) - } + val listener = MessageListChangedListener { + launch { + send(messageCountsProvider.getMessageCounts(account)) } } - contentResolver.registerContentObserver(notificationUri, false, contentObserver) + messageListRepository.addListener(account.uuid, listener) awaitClose { - contentResolver.unregisterContentObserver(contentObserver) + messageListRepository.removeListener(listener) } }.flowOn(Dispatchers.IO) } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/account/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/account/KoinModule.kt index f72738ab32..426350b5e3 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/account/KoinModule.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/account/KoinModule.kt @@ -4,7 +4,9 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val accountUiModule = module { - viewModel { AccountsViewModel(accountManager = get(), messageCountsProvider = get(), contentResolver = get()) } + viewModel { + AccountsViewModel(accountManager = get(), messageCountsProvider = get(), messageListRepository = get()) + } factory { AccountImageLoader(accountFallbackImageProvider = get()) } factory { AccountFallbackImageProvider(context = get()) } factory { AccountImageModelLoaderFactory(contactPhotoLoader = get(), accountFallbackImageProvider = get()) } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt index c24478518c..2e4718d408 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt @@ -8,5 +8,7 @@ val messageListUiModule = module { factory { DefaultFolderProvider() } factory { MessageListExtractor(get(), get()) } factory { MessageListLoader(get(), get(), get(), get()) } - factory { MessageListLiveDataFactory(get(), get(), get()) } + factory { + MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get()) + } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveData.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveData.kt index aaf36a1b08..bf757521d2 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveData.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveData.kt @@ -1,12 +1,9 @@ package com.fsck.k9.ui.messagelist -import android.content.ContentResolver -import android.database.ContentObserver -import android.net.Uri -import android.os.Handler import androidx.lifecycle.LiveData import com.fsck.k9.Preferences -import com.fsck.k9.provider.EmailProvider +import com.fsck.k9.mailstore.MessageListChangedListener +import com.fsck.k9.mailstore.MessageListRepository import com.fsck.k9.search.getAccountUuids import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -16,53 +13,43 @@ import kotlinx.coroutines.withContext class MessageListLiveData( private val messageListLoader: MessageListLoader, private val preferences: Preferences, - private val contentResolver: ContentResolver, + private val messageListRepository: MessageListRepository, private val coroutineScope: CoroutineScope, val config: MessageListConfig ) : LiveData() { - private val contentObserver = object : ContentObserver(Handler()) { - override fun onChange(selfChange: Boolean) { - loadMessageListAsync() - } + private val messageListChangedListener = MessageListChangedListener { + loadMessageListAsync() } private fun loadMessageListAsync() { coroutineScope.launch(Dispatchers.Main) { - value = withContext(Dispatchers.IO) { + val messageList = withContext(Dispatchers.IO) { messageListLoader.getMessageList(config) } + value = messageList } } override fun onActive() { super.onActive() - registerContentObserverAsync() + registerMessageListChangedListenerAsync() loadMessageListAsync() } override fun onInactive() { super.onInactive() - contentResolver.unregisterContentObserver(contentObserver) + messageListRepository.removeListener(messageListChangedListener) } - private fun registerContentObserverAsync() { - coroutineScope.launch(Dispatchers.Main) { - val notificationUris = withContext(Dispatchers.IO) { - getNotificationUris() - } + private fun registerMessageListChangedListenerAsync() { + coroutineScope.launch(Dispatchers.IO) { + val accountUuids = config.search.getAccountUuids(preferences) - for (notificationUri in notificationUris) { - contentResolver.registerContentObserver(notificationUri, false, contentObserver) + for (accountUuid in accountUuids) { + messageListRepository.addListener(accountUuid, messageListChangedListener) } } } - - private fun getNotificationUris(): List { - val accountUuids = config.search.getAccountUuids(preferences) - return accountUuids.map { accountUuid -> - EmailProvider.getNotificationUri(accountUuid) - } - } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveDataFactory.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveDataFactory.kt index 315a0199c4..af995910f1 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveDataFactory.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveDataFactory.kt @@ -1,15 +1,15 @@ package com.fsck.k9.ui.messagelist -import android.content.ContentResolver import com.fsck.k9.Preferences +import com.fsck.k9.mailstore.MessageListRepository import kotlinx.coroutines.CoroutineScope class MessageListLiveDataFactory( private val messageListLoader: MessageListLoader, private val preferences: Preferences, - private val contentResolver: ContentResolver + private val messageListRepository: MessageListRepository ) { fun create(coroutineScope: CoroutineScope, config: MessageListConfig): MessageListLiveData { - return MessageListLiveData(messageListLoader, preferences, contentResolver, coroutineScope, config) + return MessageListLiveData(messageListLoader, preferences, messageListRepository, coroutineScope, config) } } -- GitLab From 4210237dff402f30a2e6cdd534a93aa78e7ace12 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 30 Aug 2022 20:41:21 +0200 Subject: [PATCH 43/85] Rename .java to .kt --- .../{EmailProviderCacheTest.java => EmailProviderCacheTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/core/src/test/java/com/fsck/k9/cache/{EmailProviderCacheTest.java => EmailProviderCacheTest.kt} (100%) diff --git a/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.java b/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.kt similarity index 100% rename from app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.java rename to app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.kt -- GitLab From a9d03a147d106d050950b3d3ea8b0f121649f1f8 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 30 Aug 2022 20:41:22 +0200 Subject: [PATCH 44/85] Fix `EmailProviderCacheTest` --- .../com/fsck/k9/cache/EmailProviderCache.java | 9 +- .../k9/cache/EmailProviderCacheCursor.java | 4 +- .../k9/controller/MessagingController.java | 14 +- .../com/fsck/k9/provider/EmailProvider.java | 3 +- .../fsck/k9/cache/EmailProviderCacheTest.kt | 176 +++++++++--------- 5 files changed, 98 insertions(+), 108 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.java b/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.java index 66e66d2cf3..7850b407f0 100644 --- a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.java +++ b/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.java @@ -4,8 +4,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import android.content.Context; - import com.fsck.k9.DI; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.MessageListRepository; @@ -14,15 +12,10 @@ import com.fsck.k9.mailstore.MessageListRepository; * Cache to bridge the time needed to write (user-initiated) changes to the database. */ public class EmailProviderCache { - private static Context sContext; private static Map sInstances = new HashMap<>(); - public static synchronized EmailProviderCache getCache(String accountUuid, Context context) { - - if (sContext == null) { - sContext = context.getApplicationContext(); - } + public static synchronized EmailProviderCache getCache(String accountUuid) { EmailProviderCache instance = sInstances.get(accountUuid); if (instance == null) { diff --git a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCacheCursor.java b/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCacheCursor.java index e695eea7fb..7de6177e86 100644 --- a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCacheCursor.java +++ b/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCacheCursor.java @@ -28,10 +28,10 @@ public class EmailProviderCacheCursor extends CursorWrapper { private int mPosition; - public EmailProviderCacheCursor(String accountUuid, Cursor cursor, Context context) { + public EmailProviderCacheCursor(String accountUuid, Cursor cursor) { super(cursor); - mCache = EmailProviderCache.getCache(accountUuid, context); + mCache = EmailProviderCache.getCache(accountUuid); mMessageIdColumn = cursor.getColumnIndex(MessageColumns.ID); mFolderIdColumn = cursor.getColumnIndex(MessageColumns.FOLDER_ID); diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java index 409dedc6dd..69b715d6ce 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -322,12 +322,12 @@ public class MessagingController { private void suppressMessages(Account account, List messages) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid(), context); + EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); cache.hideMessages(messages); } private void unsuppressMessages(Account account, List messages) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid(), context); + EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); cache.unhideMessages(messages); } @@ -335,14 +335,14 @@ public class MessagingController { long messageId = message.getDatabaseId(); long folderId = message.getFolder().getDatabaseId(); - EmailProviderCache cache = EmailProviderCache.getCache(message.getFolder().getAccountUuid(), context); + EmailProviderCache cache = EmailProviderCache.getCache(message.getFolder().getAccountUuid()); return cache.isMessageHidden(messageId, folderId); } private void setFlagInCache(final Account account, final List messageIds, final Flag flag, final boolean newState) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid(), context); + EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); String columnName = LocalStore.getColumnNameForFlag(flag); String value = Integer.toString((newState) ? 1 : 0); cache.setValueForMessages(messageIds, columnName, value); @@ -351,7 +351,7 @@ public class MessagingController { private void removeFlagFromCache(final Account account, final List messageIds, final Flag flag) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid(), context); + EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); String columnName = LocalStore.getColumnNameForFlag(flag); cache.removeValueForMessages(messageIds, columnName); } @@ -359,7 +359,7 @@ public class MessagingController { private void setFlagForThreadsInCache(final Account account, final List threadRootIds, final Flag flag, final boolean newState) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid(), context); + EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); String columnName = LocalStore.getColumnNameForFlag(flag); String value = Integer.toString((newState) ? 1 : 0); cache.setValueForThreads(threadRootIds, columnName, value); @@ -368,7 +368,7 @@ public class MessagingController { private void removeFlagForThreadsFromCache(final Account account, final List messageIds, final Flag flag) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid(), context); + EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); String columnName = LocalStore.getColumnNameForFlag(flag); cache.removeValueForThreads(messageIds, columnName); } diff --git a/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java b/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java index 69508571ef..35388b3ef4 100644 --- a/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java +++ b/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java @@ -9,7 +9,6 @@ import java.util.Map; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; -import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.CursorWrapper; @@ -243,7 +242,7 @@ public class EmailProvider extends ContentProvider { cursor.setNotificationUri(contentResolver, getNotificationUri(accountUuid)); cursor = new SpecialColumnsCursor(new IdTrickeryCursor(cursor), projection, specialColumns); - cursor = new EmailProviderCacheCursor(accountUuid, cursor, getContext()); + cursor = new EmailProviderCacheCursor(accountUuid, cursor); break; } } diff --git a/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.kt b/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.kt index e673c332b3..d17f9bddc7 100644 --- a/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.kt +++ b/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.kt @@ -1,145 +1,143 @@ -package com.fsck.k9.cache; - - -import java.util.Collections; -import java.util.UUID; - -import android.content.Context; -import android.net.Uri; - -import com.fsck.k9.RobolectricTest; -import com.fsck.k9.mailstore.LocalFolder; -import com.fsck.k9.mailstore.LocalMessage; -import com.fsck.k9.provider.EmailProvider; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RuntimeEnvironment; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; - +package com.fsck.k9.cache + +import com.fsck.k9.mailstore.LocalFolder +import com.fsck.k9.mailstore.LocalMessage +import com.fsck.k9.mailstore.MessageListRepository +import com.google.common.truth.Truth.assertThat +import java.util.UUID +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +private const val MESSAGE_ID = 1L +private const val FOLDER_ID = 2L + +class EmailProviderCacheTest { + private val localFolder = mock { + on { databaseId } doReturn FOLDER_ID + } -public class EmailProviderCacheTest extends RobolectricTest { - private final Context context = RuntimeEnvironment.getApplication(); + private val localMessage = mock { + on { databaseId } doReturn MESSAGE_ID + on { folder } doReturn localFolder + } - private EmailProviderCache cache; - @Mock - private LocalMessage mockLocalMessage; - @Mock - private LocalFolder mockLocalMessageFolder; - private Long localMessageId = 1L; - private Long localMessageFolderId = 2L; + private val cache = EmailProviderCache.getCache(UUID.randomUUID().toString()) @Before - public void before() { - MockitoAnnotations.initMocks(this); - EmailProvider.CONTENT_URI = Uri.parse("content://test.provider.email"); - - cache = EmailProviderCache.getCache(UUID.randomUUID().toString(), context); - when(mockLocalMessage.getDatabaseId()).thenReturn(localMessageId); - when(mockLocalMessage.getFolder()).thenReturn(mockLocalMessageFolder); - when(mockLocalMessageFolder.getDatabaseId()).thenReturn(localMessageFolderId); + fun setUp() { + startKoin { + modules( + module { + single { mock() } + } + ) + } + } + + @After + fun tearDown() { + stopKoin() } @Test - public void getCache_returnsDifferentCacheForEachUUID() { - EmailProviderCache cache = EmailProviderCache.getCache("u001", context); - EmailProviderCache cache2 = EmailProviderCache.getCache("u002", context); + fun `getCache() returns different cache for each UUID`() { + val cache = EmailProviderCache.getCache("u001") - assertNotEquals(cache, cache2); + val cache2 = EmailProviderCache.getCache("u002") + + assertThat(cache2).isNotSameInstanceAs(cache) } @Test - public void getCache_returnsSameCacheForAUUID() { - EmailProviderCache cache = EmailProviderCache.getCache("u001", context); - EmailProviderCache cache2 = EmailProviderCache.getCache("u001", context); + fun `getCache() returns same cache for the same UUID`() { + val cache = EmailProviderCache.getCache("u001") + + val cache2 = EmailProviderCache.getCache("u001") - assertSame(cache, cache2); + assertThat(cache2).isSameInstanceAs(cache) } @Test - public void getValueForMessage_returnsValueSetForMessage() { - cache.setValueForMessages(Collections.singletonList(1L), "subject", "Subject"); + fun `getValueForMessage() returns value set for message`() { + cache.setValueForMessages(listOf(1L), "subject", "Subject") - String result = cache.getValueForMessage(1L, "subject"); + val result = cache.getValueForMessage(1L, "subject") - assertEquals("Subject", result); + assertThat(result).isEqualTo("Subject") } @Test - public void getValueForUnknownMessage_returnsNull() { - String result = cache.getValueForMessage(1L, "subject"); + fun `getValueForUnknownMessage() returns null`() { + val result = cache.getValueForMessage(1L, "subject") - assertNull(result); + assertThat(result).isNull() } @Test - public void getValueForUnknownMessage_returnsNullWhenRemoved() { - cache.setValueForMessages(Collections.singletonList(1L), "subject", "Subject"); - cache.removeValueForMessages(Collections.singletonList(1L), "subject"); + fun `getValueForUnknownMessage() returns null when removed`() { + cache.setValueForMessages(listOf(1L), "subject", "Subject") + cache.removeValueForMessages(listOf(1L), "subject") - String result = cache.getValueForMessage(1L, "subject"); + val result = cache.getValueForMessage(1L, "subject") - assertNull(result); + assertThat(result).isNull() } @Test - public void getValueForThread_returnsValueSetForThread() { - cache.setValueForThreads(Collections.singletonList(1L), "subject", "Subject"); + fun `getValueForThread() returns value set for thread`() { + cache.setValueForThreads(listOf(1L), "subject", "Subject") - String result = cache.getValueForThread(1L, "subject"); + val result = cache.getValueForThread(1L, "subject") - assertEquals("Subject", result); + assertThat(result).isEqualTo("Subject") } @Test - public void getValueForUnknownThread_returnsNull() { - String result = cache.getValueForThread(1L, "subject"); + fun `getValueForUnknownThread() returns null`() { + val result = cache.getValueForThread(1L, "subject") - assertNull(result); + assertThat(result).isNull() } @Test - public void getValueForUnknownThread_returnsNullWhenRemoved() { - cache.setValueForThreads(Collections.singletonList(1L), "subject", "Subject"); - cache.removeValueForThreads(Collections.singletonList(1L), "subject"); + fun `getValueForUnknownThread() returns null when removed`() { + cache.setValueForThreads(listOf(1L), "subject", "Subject") + cache.removeValueForThreads(listOf(1L), "subject") - String result = cache.getValueForThread(1L, "subject"); + val result = cache.getValueForThread(1L, "subject") - assertNull(result); + assertThat(result).isNull() } @Test - public void isMessageHidden_returnsTrueForHiddenMessage() { - cache.hideMessages(Collections.singletonList(mockLocalMessage)); + fun `isMessageHidden() returns true for hidden message`() { + cache.hideMessages(listOf(localMessage)) - boolean result = cache.isMessageHidden(localMessageId, localMessageFolderId); + val result = cache.isMessageHidden(MESSAGE_ID, FOLDER_ID) - assertTrue(result); + assertThat(result).isTrue() } @Test - public void isMessageHidden_returnsFalseForUnknownMessage() { - boolean result = cache.isMessageHidden(localMessageId, localMessageFolderId); + fun `isMessageHidden() returns false for unknown message`() { + val result = cache.isMessageHidden(MESSAGE_ID, FOLDER_ID) - assertFalse(result); + assertThat(result).isFalse() } @Test - public void isMessageHidden_returnsFalseForUnhidenMessage() { - cache.hideMessages(Collections.singletonList(mockLocalMessage)); - cache.unhideMessages(Collections.singletonList(mockLocalMessage)); + fun `isMessageHidden() returns false for unhidden message`() { + cache.hideMessages(listOf(localMessage)) + cache.unhideMessages(listOf(localMessage)) - boolean result = cache.isMessageHidden(localMessageId, localMessageFolderId); + val result = cache.isMessageHidden(MESSAGE_ID, FOLDER_ID) - assertFalse(result); + assertThat(result).isFalse() } - } -- GitLab From 33457014714c240ce12a5b7d430424087e4dfcea Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 11:28:23 +0200 Subject: [PATCH 45/85] Rename .java to .kt --- .../k9/cache/{EmailProviderCache.java => EmailProviderCache.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/core/src/main/java/com/fsck/k9/cache/{EmailProviderCache.java => EmailProviderCache.kt} (100%) diff --git a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.java b/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.kt similarity index 100% rename from app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.java rename to app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.kt -- GitLab From b89f8c0e57ecc5abb409930397939fc8e2bc5e3c Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 11:28:23 +0200 Subject: [PATCH 46/85] Convert `EmailProviderCache` to Kotlin --- .../com/fsck/k9/cache/EmailProviderCache.kt | 183 ++++++++---------- 1 file changed, 84 insertions(+), 99 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.kt b/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.kt index 7850b407f0..a94584e2f2 100644 --- a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.kt +++ b/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.kt @@ -1,148 +1,133 @@ -package com.fsck.k9.cache; +package com.fsck.k9.cache -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import com.fsck.k9.DI +import com.fsck.k9.mailstore.LocalMessage +import com.fsck.k9.mailstore.MessageListRepository +import kotlin.collections.set -import com.fsck.k9.DI; -import com.fsck.k9.mailstore.LocalMessage; -import com.fsck.k9.mailstore.MessageListRepository; +typealias MessageId = Long +typealias ThreadId = Long +typealias FolderId = Long +typealias ColumnName = String +typealias ColumnValue = String +typealias AccountUuid = String /** * Cache to bridge the time needed to write (user-initiated) changes to the database. */ -public class EmailProviderCache { - private static Map sInstances = - new HashMap<>(); - - public static synchronized EmailProviderCache getCache(String accountUuid) { - - EmailProviderCache instance = sInstances.get(accountUuid); - if (instance == null) { - instance = new EmailProviderCache(accountUuid); - sInstances.put(accountUuid, instance); - } - - return instance; - } - - - private String mAccountUuid; - private final Map> mMessageCache = new HashMap<>(); - private final Map> mThreadCache = new HashMap<>(); - private final Map mHiddenMessageCache = new HashMap<>(); - - - private EmailProviderCache(String accountUuid) { - mAccountUuid = accountUuid; - } - - public String getValueForMessage(Long messageId, String columnName) { - synchronized (mMessageCache) { - Map map = mMessageCache.get(messageId); - return (map == null) ? null : map.get(columnName); +class EmailProviderCache private constructor(private val accountUuid: String) { + private val messageCache = mutableMapOf>() + private val threadCache = mutableMapOf>() + private val hiddenMessageCache = mutableMapOf() + + fun getValueForMessage(messageId: Long, columnName: String): String? { + synchronized(messageCache) { + val columnMap = messageCache[messageId] + return columnMap?.get(columnName) } } - public String getValueForThread(Long threadRootId, String columnName) { - synchronized (mThreadCache) { - Map map = mThreadCache.get(threadRootId); - return (map == null) ? null : map.get(columnName); + fun getValueForThread(threadRootId: Long, columnName: String): String? { + synchronized(threadCache) { + val columnMap = threadCache[threadRootId] + return columnMap?.get(columnName) } } - public void setValueForMessages(List messageIds, String columnName, String value) { - synchronized (mMessageCache) { - for (Long messageId : messageIds) { - Map map = mMessageCache.get(messageId); - if (map == null) { - map = new HashMap<>(); - mMessageCache.put(messageId, map); - } - map.put(columnName, value); + fun setValueForMessages(messageIds: List, columnName: String, value: String) { + synchronized(messageCache) { + for (messageId in messageIds) { + val columnMap = messageCache.getOrPut(messageId) { mutableMapOf() } + columnMap[columnName] = value } } - notifyChange(); + notifyChange() } - public void setValueForThreads(List threadRootIds, String columnName, String value) { - synchronized (mThreadCache) { - for (Long threadRootId : threadRootIds) { - Map map = mThreadCache.get(threadRootId); - if (map == null) { - map = new HashMap<>(); - mThreadCache.put(threadRootId, map); - } - map.put(columnName, value); + fun setValueForThreads(threadRootIds: List, columnName: String, value: String) { + synchronized(threadCache) { + for (threadRootId in threadRootIds) { + val columnMap = threadCache.getOrPut(threadRootId) { mutableMapOf() } + columnMap[columnName] = value } } - notifyChange(); + notifyChange() } - public void removeValueForMessages(List messageIds, String columnName) { - synchronized (mMessageCache) { - for (Long messageId : messageIds) { - Map map = mMessageCache.get(messageId); - if (map != null) { - map.remove(columnName); - if (map.isEmpty()) { - mMessageCache.remove(messageId); + fun removeValueForMessages(messageIds: List, columnName: String) { + synchronized(messageCache) { + for (messageId in messageIds) { + val columnMap = messageCache[messageId] + if (columnMap != null) { + columnMap.remove(columnName) + if (columnMap.isEmpty()) { + messageCache.remove(messageId) } } } } } - public void removeValueForThreads(List threadRootIds, String columnName) { - synchronized (mThreadCache) { - for (Long threadRootId : threadRootIds) { - Map map = mThreadCache.get(threadRootId); - if (map != null) { - map.remove(columnName); - if (map.isEmpty()) { - mThreadCache.remove(threadRootId); + fun removeValueForThreads(threadRootIds: List, columnName: String) { + synchronized(threadCache) { + for (threadRootId in threadRootIds) { + val columnMap = threadCache[threadRootId] + if (columnMap != null) { + columnMap.remove(columnName) + if (columnMap.isEmpty()) { + threadCache.remove(threadRootId) } } } } } - public void hideMessages(List messages) { - synchronized (mHiddenMessageCache) { - for (LocalMessage message : messages) { - long messageId = message.getDatabaseId(); - mHiddenMessageCache.put(messageId, message.getFolder().getDatabaseId()); + fun hideMessages(messages: List) { + synchronized(hiddenMessageCache) { + for (message in messages) { + val messageId = message.databaseId + val folderId = message.folder.databaseId + hiddenMessageCache[messageId] = folderId } } - notifyChange(); + notifyChange() } - public boolean isMessageHidden(Long messageId, long folderId) { - synchronized (mHiddenMessageCache) { - Long hiddenInFolder = mHiddenMessageCache.get(messageId); - return (hiddenInFolder != null && hiddenInFolder == folderId); + fun isMessageHidden(messageId: Long, folderId: Long): Boolean { + synchronized(hiddenMessageCache) { + val hiddenInFolder = hiddenMessageCache[messageId] + return hiddenInFolder == folderId } } - public void unhideMessages(List messages) { - synchronized (mHiddenMessageCache) { - for (LocalMessage message : messages) { - long messageId = message.getDatabaseId(); - long folderId = message.getFolder().getDatabaseId(); - Long hiddenInFolder = mHiddenMessageCache.get(messageId); - - if (hiddenInFolder != null && hiddenInFolder == folderId) { - mHiddenMessageCache.remove(messageId); + fun unhideMessages(messages: List) { + synchronized(hiddenMessageCache) { + for (message in messages) { + val messageId = message.databaseId + val folderId = message.folder.databaseId + val hiddenInFolder = hiddenMessageCache[messageId] + if (hiddenInFolder == folderId) { + hiddenMessageCache.remove(messageId) } } } } - private void notifyChange() { - MessageListRepository messageListRepository = DI.get(MessageListRepository.class); - messageListRepository.notifyMessageListChanged(mAccountUuid); + private fun notifyChange() { + val messageListRepository = DI.get() + messageListRepository.notifyMessageListChanged(accountUuid) + } + + companion object { + private val instances = mutableMapOf() + + @JvmStatic + @Synchronized + fun getCache(accountUuid: String): EmailProviderCache { + return instances.getOrPut(accountUuid) { EmailProviderCache(accountUuid) } + } } } -- GitLab From 170f0dbeccc8f848e6c85f404e0cb67ac3016315 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 14:20:35 +0200 Subject: [PATCH 47/85] Add `MessageListRepository.getMessages()` --- .../k9/mailstore/CacheAwareMessageMapper.kt | 53 +++++ .../java/com/fsck/k9/mailstore/KoinModule.kt | 2 +- .../k9/mailstore/MessageListRepository.kt | 22 +- .../com/fsck/k9/mailstore/MessageStore.kt | 2 +- .../k9/mailstore/MessageListRepositoryTest.kt | 189 +++++++++++++++++- .../k9/storage/messages/K9MessageStore.kt | 2 +- .../messages/RetrieveMessageListOperations.kt | 11 +- .../RetrieveMessageListOperationsTest.kt | 2 +- 8 files changed, 268 insertions(+), 15 deletions(-) create mode 100644 app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt b/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt new file mode 100644 index 0000000000..956b1ce563 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt @@ -0,0 +1,53 @@ +package com.fsck.k9.mailstore + +import com.fsck.k9.cache.EmailProviderCache + +internal class CacheAwareMessageMapper( + private val cache: EmailProviderCache, + private val messageMapper: MessageMapper +) : MessageMapper { + override fun map(message: MessageDetailsAccessor): T? { + val messageId = message.id + val folderId = message.folderId + + if (cache.isMessageHidden(messageId, folderId)) { + return null + } + + val cachedMessage = CacheAwareMessageDetailsAccessor(cache, message) + return messageMapper.map(cachedMessage) + } +} + +private class CacheAwareMessageDetailsAccessor( + private val cache: EmailProviderCache, + private val message: MessageDetailsAccessor +) : MessageDetailsAccessor by message { + override val isRead: Boolean + get() { + return cache.getValueForMessage(message.id, "read")?.let { it == "1" } + ?: cache.getValueForThread(message.threadRoot, "read")?.let { it == "1" } + ?: message.isRead + } + + override val isStarred: Boolean + get() { + return cache.getValueForMessage(message.id, "flagged")?.let { it == "1" } + ?: cache.getValueForThread(message.threadRoot, "flagged")?.let { it == "1" } + ?: message.isStarred + } + + override val isAnswered: Boolean + get() { + return cache.getValueForMessage(message.id, "answered")?.let { it == "1" } + ?: cache.getValueForThread(message.threadRoot, "answered")?.let { it == "1" } + ?: message.isAnswered + } + + override val isForwarded: Boolean + get() { + return cache.getValueForMessage(message.id, "forwarded")?.let { it == "1" } + ?: cache.getValueForThread(message.threadRoot, "forwarded")?.let { it == "1" } + ?: message.isForwarded + } +} diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt index e2465297af..8575c3bf55 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt @@ -34,5 +34,5 @@ val mailStoreModule = module { attachmentCounter = get() ) } - single { MessageListRepository() } + single { MessageListRepository(messageStoreManager = get()) } } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt index d852bd9451..30a9a90b7e 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt @@ -1,8 +1,11 @@ package com.fsck.k9.mailstore +import com.fsck.k9.cache.EmailProviderCache import java.util.concurrent.CopyOnWriteArraySet -class MessageListRepository { +class MessageListRepository( + private val messageStoreManager: MessageStoreManager +) { private val listeners = CopyOnWriteArraySet>() fun addListener(accountUuid: String, listener: MessageListChangedListener) { @@ -21,6 +24,23 @@ class MessageListRepository { } } } + + /** + * Retrieve list of messages from [MessageStore] but override values with data from [EmailProviderCache]. + */ + fun getMessages( + accountUuid: String, + selection: String, + selectionArgs: Array, + sortOrder: String, + messageMapper: MessageMapper + ): List { + val messageStore = messageStoreManager.getMessageStore(accountUuid) + val cache = EmailProviderCache.getCache(accountUuid) + + val mapper = CacheAwareMessageMapper(cache, messageMapper) + return messageStore.getMessages(selection, selectionArgs, sortOrder, mapper) + } } fun interface MessageListChangedListener { diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt index 91d8f8bf70..f68e6c7e0c 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt @@ -127,7 +127,7 @@ interface MessageStore { selection: String, selectionArgs: Array, sortOrder: String, - messageMapper: MessageMapper + messageMapper: MessageMapper ): List /** diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt index e32fbe8678..5c8572b443 100644 --- a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt +++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt @@ -1,12 +1,58 @@ package com.fsck.k9.mailstore +import com.fsck.k9.cache.EmailProviderCache +import com.fsck.k9.mail.Address +import com.fsck.k9.message.extractors.PreviewResult import com.google.common.truth.Truth.assertThat +import java.util.UUID +import org.junit.After +import org.junit.Before import org.junit.Test +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub -private const val ACCOUNT_UUID = "00000000-0000-4000-0000-000000000000" +private const val MESSAGE_ID = 1L +private const val MESSAGE_ID_2 = 2L +private const val FOLDER_ID = 20L +private const val FOLDER_ID_2 = 21L +private const val THREAD_ROOT = 30L + +private const val SELECTION = "irrelevant" +private val SELECTION_ARGS = arrayOf("irrelevant") +private const val SORT_ORDER = "irrelevant" class MessageListRepositoryTest { - private val messageListRepository = MessageListRepository() + private val accountUuid = UUID.randomUUID().toString() + + private val messageStore = mock() + private val messageStoreManager = mock { + on { getMessageStore(accountUuid) } doReturn messageStore + } + + private val messageListRepository = MessageListRepository(messageStoreManager) + + @Before + fun setUp() { + startKoin { + modules( + module { + single { messageListRepository } + } + ) + } + } + + @After + fun tearDown() { + stopKoin() + } @Test fun `adding and removing listener`() { @@ -14,15 +60,15 @@ class MessageListRepositoryTest { val listener = MessageListChangedListener { messageListChanged++ } - messageListRepository.addListener(ACCOUNT_UUID, listener) + messageListRepository.addListener(accountUuid, listener) - messageListRepository.notifyMessageListChanged(ACCOUNT_UUID) + messageListRepository.notifyMessageListChanged(accountUuid) assertThat(messageListChanged).isEqualTo(1) messageListRepository.removeListener(listener) - messageListRepository.notifyMessageListChanged(ACCOUNT_UUID) + messageListRepository.notifyMessageListChanged(accountUuid) assertThat(messageListChanged).isEqualTo(1) } @@ -33,7 +79,7 @@ class MessageListRepositoryTest { val listener = MessageListChangedListener { messageListChanged++ } - messageListRepository.addListener(ACCOUNT_UUID, listener) + messageListRepository.addListener(accountUuid, listener) messageListRepository.notifyMessageListChanged("otherAccountUuid") @@ -42,6 +88,135 @@ class MessageListRepositoryTest { @Test fun `notifyMessageListChanged() without any listeners should not throw`() { - messageListRepository.notifyMessageListChanged(ACCOUNT_UUID) + messageListRepository.notifyMessageListChanged(accountUuid) + } + + @Test + fun `getMessages() should use flag values from the cache`() { + addMessages( + MessageData( + messageId = MESSAGE_ID, + folderId = FOLDER_ID, + threadRoot = THREAD_ROOT, + isRead = false, + isStarred = true, + isAnswered = false, + isForwarded = true + ) + ) + EmailProviderCache.getCache(accountUuid).apply { + setValueForMessages(listOf(MESSAGE_ID), "read", "1") + setValueForThreads(listOf(THREAD_ROOT), "flagged", "0") + } + + val result = messageListRepository.getMessages(accountUuid, SELECTION, SELECTION_ARGS, SORT_ORDER) { message -> + MessageData( + messageId = message.id, + folderId = message.folderId, + threadRoot = message.threadRoot, + isRead = message.isRead, + isStarred = message.isStarred, + isAnswered = message.isAnswered, + isForwarded = message.isForwarded + ) + } + + assertThat(result).containsExactly( + MessageData( + messageId = MESSAGE_ID, + folderId = FOLDER_ID, + threadRoot = THREAD_ROOT, + isRead = true, + isStarred = false, + isAnswered = false, + isForwarded = true + ) + ) + } + + @Test + fun `getMessages() should skip messages marked as hidden in the cache`() { + addMessages( + MessageData(messageId = MESSAGE_ID, folderId = FOLDER_ID, threadRoot = THREAD_ROOT), + MessageData(messageId = MESSAGE_ID_2, folderId = FOLDER_ID, threadRoot = THREAD_ROOT) + ) + hideMessage(MESSAGE_ID, FOLDER_ID) + + val result = messageListRepository.getMessages(accountUuid, SELECTION, SELECTION_ARGS, SORT_ORDER) { message -> + message.id + } + + assertThat(result).containsExactly(MESSAGE_ID_2) + } + + @Test + fun `getMessages() should not skip message when marked as hidden in a different folder`() { + addMessages( + MessageData(messageId = MESSAGE_ID, folderId = FOLDER_ID, threadRoot = THREAD_ROOT), + MessageData(messageId = MESSAGE_ID_2, folderId = FOLDER_ID, threadRoot = THREAD_ROOT) + ) + hideMessage(MESSAGE_ID, FOLDER_ID_2) + + val result = messageListRepository.getMessages(accountUuid, SELECTION, SELECTION_ARGS, SORT_ORDER) { message -> + message.id + } + + assertThat(result).containsExactly(MESSAGE_ID, MESSAGE_ID_2) + } + + private fun addMessages(vararg messages: MessageData) { + messageStore.stub { + on { getMessages(eq(SELECTION), eq(SELECTION_ARGS), eq(SORT_ORDER), any()) } doAnswer { + val mapper: MessageMapper = it.getArgument(3) + + messages.mapNotNull { message -> + mapper.map(object : MessageDetailsAccessor { + override val id = message.messageId + override val messageServerId = "irrelevant" + override val folderId = message.folderId + override val fromAddresses = emptyList
() + override val toAddresses = emptyList
() + override val ccAddresses = emptyList
() + override val messageDate = 0L + override val internalDate = 0L + override val subject = "irrelevant" + override val preview = PreviewResult.error() + override val isRead = message.isRead + override val isStarred = message.isStarred + override val isAnswered = message.isAnswered + override val isForwarded = message.isForwarded + override val hasAttachments = false + override val threadRoot = message.threadRoot + override val threadCount = 0 + }) + } + } + } + } + + @Suppress("SameParameterValue") + private fun hideMessage(messageId: Long, folderId: Long) { + val cache = EmailProviderCache.getCache(accountUuid) + + val localFolder = mock { + on { databaseId } doReturn folderId + } + + val localMessage = mock { + on { databaseId } doReturn messageId + on { folder } doReturn localFolder + } + + cache.hideMessages(listOf(localMessage)) } } + +private data class MessageData( + val messageId: Long, + val folderId: Long, + val threadRoot: Long, + val isRead: Boolean = false, + val isStarred: Boolean = false, + val isAnswered: Boolean = false, + val isForwarded: Boolean = false, +) diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt index 239f5aab3f..ec41e5c521 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt @@ -106,7 +106,7 @@ class K9MessageStore( selection: String, selectionArgs: Array, sortOrder: String, - messageMapper: MessageMapper + messageMapper: MessageMapper ): List { return retrieveMessageListOperations.getMessages(selection, selectionArgs, sortOrder, messageMapper) } diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt index 896d2ee5df..4171c1e79f 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt @@ -16,7 +16,7 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka selection: String, selectionArgs: Array, sortOrder: String, - mapper: MessageMapper + mapper: MessageMapper ): List { return lockableDatabase.execute(false) { database -> database.rawQuery( @@ -50,8 +50,13 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka selectionArgs, ).use { cursor -> val cursorMessageAccessor = CursorMessageAccessor(cursor, includesThreadCount = false) - cursor.map { - mapper.map(cursorMessageAccessor) + buildList { + while (cursor.moveToNext()) { + val value = mapper.map(cursorMessageAccessor) + if (value != null) { + add(value) + } + } } } } diff --git a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt index 8e8db34f39..fe767b2510 100644 --- a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt +++ b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt @@ -466,7 +466,7 @@ class RetrieveMessageListOperationsTest : RobolectricTest() { assertThat(result).containsExactly(messageId2, messageId3) } - private fun getMessagesFromFolder(folderId: Long, mapper: MessageMapper): List { + private fun getMessagesFromFolder(folderId: Long, mapper: MessageMapper): List { return retrieveMessageListOperations.getMessages( selection = "folder_id = ?", selectionArgs = arrayOf(folderId.toString()), -- GitLab From 4b03f99ff73b4dcf838384cb1fdb4e406d741cfd Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 14:46:19 +0200 Subject: [PATCH 48/85] Add `MessageListRepository.getThreadedMessages()` --- .../k9/mailstore/MessageListRepository.kt | 17 ++ .../com/fsck/k9/mailstore/MessageStore.kt | 2 +- .../k9/mailstore/MessageListRepositoryTest.kt | 145 +++++++++++++++--- .../k9/storage/messages/K9MessageStore.kt | 2 +- .../messages/RetrieveMessageListOperations.kt | 11 +- .../RetrieveMessageListOperationsTest.kt | 2 +- 6 files changed, 152 insertions(+), 27 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt index 30a9a90b7e..e96c99aa20 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt @@ -41,6 +41,23 @@ class MessageListRepository( val mapper = CacheAwareMessageMapper(cache, messageMapper) return messageStore.getMessages(selection, selectionArgs, sortOrder, mapper) } + + /** + * Retrieve threaded list of messages from [MessageStore] but override values with data from [EmailProviderCache]. + */ + fun getThreadedMessages( + accountUuid: String, + selection: String, + selectionArgs: Array, + sortOrder: String, + messageMapper: MessageMapper + ): List { + val messageStore = messageStoreManager.getMessageStore(accountUuid) + val cache = EmailProviderCache.getCache(accountUuid) + + val mapper = CacheAwareMessageMapper(cache, messageMapper) + return messageStore.getThreadedMessages(selection, selectionArgs, sortOrder, mapper) + } } fun interface MessageListChangedListener { diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt index f68e6c7e0c..2b07a9b2fb 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt @@ -137,7 +137,7 @@ interface MessageStore { selection: String, selectionArgs: Array, sortOrder: String, - messageMapper: MessageMapper + messageMapper: MessageMapper ): List /** diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt index 5c8572b443..6477c51d49 100644 --- a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt +++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt @@ -23,6 +23,7 @@ private const val MESSAGE_ID_2 = 2L private const val FOLDER_ID = 20L private const val FOLDER_ID_2 = 21L private const val THREAD_ROOT = 30L +private const val THREAD_ROOT_2 = 31L private const val SELECTION = "irrelevant" private val SELECTION_ARGS = arrayOf("irrelevant") @@ -164,36 +165,138 @@ class MessageListRepositoryTest { assertThat(result).containsExactly(MESSAGE_ID, MESSAGE_ID_2) } + @Test + fun `getThreadedMessages() should use flag values from the cache`() { + addThreadedMessages( + MessageData( + messageId = MESSAGE_ID, + folderId = FOLDER_ID, + threadRoot = THREAD_ROOT, + isRead = false, + isStarred = true, + isAnswered = false, + isForwarded = true + ) + ) + EmailProviderCache.getCache(accountUuid).apply { + setValueForMessages(listOf(MESSAGE_ID), "read", "1") + setValueForThreads(listOf(THREAD_ROOT), "flagged", "0") + } + + val result = messageListRepository.getThreadedMessages( + accountUuid, + SELECTION, + SELECTION_ARGS, + SORT_ORDER + ) { message -> + MessageData( + messageId = message.id, + folderId = message.folderId, + threadRoot = message.threadRoot, + isRead = message.isRead, + isStarred = message.isStarred, + isAnswered = message.isAnswered, + isForwarded = message.isForwarded + ) + } + + assertThat(result).containsExactly( + MessageData( + messageId = MESSAGE_ID, + folderId = FOLDER_ID, + threadRoot = THREAD_ROOT, + isRead = true, + isStarred = false, + isAnswered = false, + isForwarded = true + ) + ) + } + + @Test + fun `getThreadedMessages() should skip messages marked as hidden in the cache`() { + addThreadedMessages( + MessageData(messageId = MESSAGE_ID, folderId = FOLDER_ID, threadRoot = THREAD_ROOT), + MessageData(messageId = MESSAGE_ID_2, folderId = FOLDER_ID, threadRoot = THREAD_ROOT_2) + ) + hideMessage(MESSAGE_ID, FOLDER_ID) + + val result = messageListRepository.getThreadedMessages( + accountUuid, + SELECTION, + SELECTION_ARGS, + SORT_ORDER + ) { message -> + message.id + } + + assertThat(result).containsExactly(MESSAGE_ID_2) + } + + @Test + fun `getThreadedMessages() should not skip message when marked as hidden in a different folder`() { + addThreadedMessages( + MessageData(messageId = MESSAGE_ID, folderId = FOLDER_ID, threadRoot = THREAD_ROOT), + MessageData(messageId = MESSAGE_ID_2, folderId = FOLDER_ID, threadRoot = THREAD_ROOT_2) + ) + hideMessage(MESSAGE_ID, FOLDER_ID_2) + + val result = messageListRepository.getThreadedMessages( + accountUuid, + SELECTION, + SELECTION_ARGS, + SORT_ORDER + ) { message -> + message.id + } + + assertThat(result).containsExactly(MESSAGE_ID, MESSAGE_ID_2) + } + private fun addMessages(vararg messages: MessageData) { messageStore.stub { on { getMessages(eq(SELECTION), eq(SELECTION_ARGS), eq(SORT_ORDER), any()) } doAnswer { val mapper: MessageMapper = it.getArgument(3) - messages.mapNotNull { message -> - mapper.map(object : MessageDetailsAccessor { - override val id = message.messageId - override val messageServerId = "irrelevant" - override val folderId = message.folderId - override val fromAddresses = emptyList
() - override val toAddresses = emptyList
() - override val ccAddresses = emptyList
() - override val messageDate = 0L - override val internalDate = 0L - override val subject = "irrelevant" - override val preview = PreviewResult.error() - override val isRead = message.isRead - override val isStarred = message.isStarred - override val isAnswered = message.isAnswered - override val isForwarded = message.isForwarded - override val hasAttachments = false - override val threadRoot = message.threadRoot - override val threadCount = 0 - }) - } + runMessageMapper(messages, mapper) + } + } + } + + private fun addThreadedMessages(vararg messages: MessageData) { + messageStore.stub { + on { getThreadedMessages(eq(SELECTION), eq(SELECTION_ARGS), eq(SORT_ORDER), any()) } doAnswer { + val mapper: MessageMapper = it.getArgument(3) + + runMessageMapper(messages, mapper) } } } + private fun runMessageMapper(messages: Array, mapper: MessageMapper): List { + return messages.mapNotNull { message -> + mapper.map(object : MessageDetailsAccessor { + override val id = message.messageId + override val messageServerId = "irrelevant" + override val folderId = message.folderId + override val fromAddresses = emptyList
() + override val toAddresses = emptyList
() + override val ccAddresses = emptyList
() + override val messageDate = 0L + override val internalDate = 0L + override val subject = "irrelevant" + override val preview = PreviewResult.error() + override val isRead = message.isRead + override val isStarred = message.isStarred + override val isAnswered = message.isAnswered + override val isForwarded = message.isForwarded + override val hasAttachments = false + override val threadRoot = message.threadRoot + override val threadCount = 0 + }) + } + } + @Suppress("SameParameterValue") private fun hideMessage(messageId: Long, folderId: Long) { val cache = EmailProviderCache.getCache(accountUuid) diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt index ec41e5c521..7b515d4d1b 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt @@ -115,7 +115,7 @@ class K9MessageStore( selection: String, selectionArgs: Array, sortOrder: String, - messageMapper: MessageMapper + messageMapper: MessageMapper ): List { return retrieveMessageListOperations.getThreadedMessages(selection, selectionArgs, sortOrder, messageMapper) } diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt index 4171c1e79f..25d02552a1 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt @@ -66,7 +66,7 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka selection: String, selectionArgs: Array, sortOrder: String, - mapper: MessageMapper + mapper: MessageMapper ): List { val orderBy = SqlQueryBuilder.addPrefixToSelection(AGGREGATED_MESSAGES_COLUMNS, "aggregated.", sortOrder) @@ -130,8 +130,13 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka selectionArgs, ).use { cursor -> val cursorMessageAccessor = CursorMessageAccessor(cursor, includesThreadCount = true) - cursor.map { - mapper.map(cursorMessageAccessor) + buildList { + while (cursor.moveToNext()) { + val value = mapper.map(cursorMessageAccessor) + if (value != null) { + add(value) + } + } } } } diff --git a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt index fe767b2510..45b62c19d5 100644 --- a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt +++ b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageListOperationsTest.kt @@ -475,7 +475,7 @@ class RetrieveMessageListOperationsTest : RobolectricTest() { ) } - private fun getThreadedMessagesFromFolder(folderId: Long, mapper: MessageMapper): List { + private fun getThreadedMessagesFromFolder(folderId: Long, mapper: MessageMapper): List { return retrieveMessageListOperations.getThreadedMessages( selection = "folder_id = ?", selectionArgs = arrayOf(folderId.toString()), -- GitLab From dabb398e65b202333ee0ceb05d0e367e14c2f926 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 14:56:41 +0200 Subject: [PATCH 49/85] Add `MessageListRepository.getThread()` --- .../k9/mailstore/MessageListRepository.kt | 16 +++ .../com/fsck/k9/mailstore/MessageStore.kt | 2 +- .../k9/mailstore/MessageListRepositoryTest.kt | 108 ++++++++++++++++++ .../k9/storage/messages/K9MessageStore.kt | 2 +- .../messages/RetrieveMessageListOperations.kt | 11 +- 5 files changed, 134 insertions(+), 5 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt index e96c99aa20..82abe2fc9a 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt @@ -58,6 +58,22 @@ class MessageListRepository( val mapper = CacheAwareMessageMapper(cache, messageMapper) return messageStore.getThreadedMessages(selection, selectionArgs, sortOrder, mapper) } + + /** + * Retrieve list of messages in a thread from [MessageStore] but override values with data from [EmailProviderCache]. + */ + fun getThread( + accountUuid: String, + threadId: Long, + sortOrder: String, + messageMapper: MessageMapper + ): List { + val messageStore = messageStoreManager.getMessageStore(accountUuid) + val cache = EmailProviderCache.getCache(accountUuid) + + val mapper = CacheAwareMessageMapper(cache, messageMapper) + return messageStore.getThread(threadId, sortOrder, mapper) + } } fun interface MessageListChangedListener { diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt index 2b07a9b2fb..7f85c5638c 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt @@ -143,7 +143,7 @@ interface MessageStore { /** * Retrieve list of messages in a thread. */ - fun getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper): List + fun getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper): List /** * Retrieve the date of the oldest message in the given folder. diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt index 6477c51d49..0f9735381a 100644 --- a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt +++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt @@ -20,6 +20,7 @@ import org.mockito.kotlin.stub private const val MESSAGE_ID = 1L private const val MESSAGE_ID_2 = 2L +private const val MESSAGE_ID_3 = 3L private const val FOLDER_ID = 20L private const val FOLDER_ID_2 = 21L private const val THREAD_ROOT = 30L @@ -253,6 +254,102 @@ class MessageListRepositoryTest { assertThat(result).containsExactly(MESSAGE_ID, MESSAGE_ID_2) } + @Test + fun `getThread() should use flag values from the cache`() { + addMessagesToThread( + THREAD_ROOT, + MessageData( + messageId = MESSAGE_ID, + folderId = FOLDER_ID, + threadRoot = THREAD_ROOT, + isRead = false, + isStarred = true, + isAnswered = false, + isForwarded = true + ), + MessageData( + messageId = MESSAGE_ID_2, + folderId = FOLDER_ID, + threadRoot = THREAD_ROOT, + isRead = false, + isStarred = true, + isAnswered = true, + isForwarded = false + ) + ) + EmailProviderCache.getCache(accountUuid).apply { + setValueForMessages(listOf(MESSAGE_ID), "read", "1") + setValueForThreads(listOf(THREAD_ROOT), "flagged", "0") + } + + val result = messageListRepository.getThread( + accountUuid, + THREAD_ROOT, + SORT_ORDER + ) { message -> + MessageData( + messageId = message.id, + folderId = message.folderId, + threadRoot = message.threadRoot, + isRead = message.isRead, + isStarred = message.isStarred, + isAnswered = message.isAnswered, + isForwarded = message.isForwarded + ) + } + + assertThat(result).containsExactly( + MessageData( + messageId = MESSAGE_ID, + folderId = FOLDER_ID, + threadRoot = THREAD_ROOT, + isRead = true, + isStarred = false, + isAnswered = false, + isForwarded = true + ), + MessageData( + messageId = MESSAGE_ID_2, + folderId = FOLDER_ID, + threadRoot = THREAD_ROOT, + isRead = false, + isStarred = false, + isAnswered = true, + isForwarded = false + ) + ) + } + + @Test + fun `getThread() should skip messages marked as hidden in the cache`() { + addMessagesToThread( + THREAD_ROOT, + MessageData(messageId = MESSAGE_ID, folderId = FOLDER_ID, threadRoot = THREAD_ROOT), + MessageData(messageId = MESSAGE_ID_2, folderId = FOLDER_ID, threadRoot = THREAD_ROOT), + MessageData(messageId = MESSAGE_ID_3, folderId = FOLDER_ID, threadRoot = THREAD_ROOT) + ) + hideMessage(MESSAGE_ID, FOLDER_ID) + + val result = messageListRepository.getThread(accountUuid, THREAD_ROOT, SORT_ORDER) { message -> message.id } + + assertThat(result).containsExactly(MESSAGE_ID_2, MESSAGE_ID_3) + } + + @Test + fun `getThread() should not skip message when marked as hidden in a different folder`() { + addMessagesToThread( + THREAD_ROOT, + MessageData(messageId = MESSAGE_ID, folderId = FOLDER_ID, threadRoot = THREAD_ROOT), + MessageData(messageId = MESSAGE_ID_2, folderId = FOLDER_ID, threadRoot = THREAD_ROOT), + MessageData(messageId = MESSAGE_ID_3, folderId = FOLDER_ID, threadRoot = THREAD_ROOT) + ) + hideMessage(MESSAGE_ID, FOLDER_ID_2) + + val result = messageListRepository.getThread(accountUuid, THREAD_ROOT, SORT_ORDER) { message -> message.id } + + assertThat(result).containsExactly(MESSAGE_ID, MESSAGE_ID_2, MESSAGE_ID_3) + } + private fun addMessages(vararg messages: MessageData) { messageStore.stub { on { getMessages(eq(SELECTION), eq(SELECTION_ARGS), eq(SORT_ORDER), any()) } doAnswer { @@ -273,6 +370,17 @@ class MessageListRepositoryTest { } } + @Suppress("SameParameterValue") + private fun addMessagesToThread(threadRoot: Long, vararg messages: MessageData) { + messageStore.stub { + on { getThread(eq(threadRoot), eq(SORT_ORDER), any()) } doAnswer { + val mapper: MessageMapper = it.getArgument(2) + + runMessageMapper(messages, mapper) + } + } + } + private fun runMessageMapper(messages: Array, mapper: MessageMapper): List { return messages.mapNotNull { message -> mapper.map(object : MessageDetailsAccessor { diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt index 7b515d4d1b..60b4f0ee76 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt @@ -120,7 +120,7 @@ class K9MessageStore( return retrieveMessageListOperations.getThreadedMessages(selection, selectionArgs, sortOrder, messageMapper) } - override fun getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper): List { + override fun getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper): List { return retrieveMessageListOperations.getThread(threadId, sortOrder, messageMapper) } diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt index 25d02552a1..3dff6cc596 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt @@ -142,7 +142,7 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka } } - fun getThread(threadId: Long, sortOrder: String, mapper: MessageMapper): List { + fun getThread(threadId: Long, sortOrder: String, mapper: MessageMapper): List { return lockableDatabase.execute(false) { database -> database.rawQuery( """ @@ -175,8 +175,13 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka arrayOf(threadId.toString()), ).use { cursor -> val cursorMessageAccessor = CursorMessageAccessor(cursor, includesThreadCount = false) - cursor.map { - mapper.map(cursorMessageAccessor) + buildList { + while (cursor.moveToNext()) { + val value = mapper.map(cursorMessageAccessor) + if (value != null) { + add(value) + } + } } } } -- GitLab From 6be1eb11dc3e606b1ebce1826517eaf6bd3d6f2c Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 16:30:22 +0200 Subject: [PATCH 50/85] Use `MessageRepository` instead of `EmailProvider` in `MessageListLoader` --- .../com/fsck/k9/helper/MessageHelper.java | 3 +- .../com/fsck/k9/mailstore/MessageColumns.kt | 24 ++ .../com/fsck/k9/ui/messagelist/KoinModule.kt | 10 +- .../k9/ui/messagelist/MessageListExtractor.kt | 94 ------- .../fsck/k9/ui/messagelist/MessageListItem.kt | 1 + .../ui/messagelist/MessageListItemMapper.kt | 60 +++++ .../k9/ui/messagelist/MessageListLoader.kt | 237 ++++++++---------- .../k9/fragment/MessageListAdapterTest.kt | 2 + 8 files changed, 196 insertions(+), 235 deletions(-) create mode 100644 app/core/src/main/java/com/fsck/k9/mailstore/MessageColumns.kt delete mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt create mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemMapper.kt diff --git a/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.java b/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.java index cb9e865dad..a01c25477c 100644 --- a/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.java +++ b/app/core/src/main/java/com/fsck/k9/helper/MessageHelper.java @@ -1,6 +1,7 @@ package com.fsck.k9.helper; +import java.util.List; import java.util.regex.Pattern; import android.content.Context; @@ -68,7 +69,7 @@ public class MessageHelper { return new SpannableStringBuilder(resourceProvider.contactDisplayNamePrefix()).append(recipients); } - public boolean toMe(Account account, Address[] toAddrs) { + public boolean toMe(Account account, List
toAddrs) { for (Address address : toAddrs) { if (account.isAnIdentity(address)) { return true; diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageColumns.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageColumns.kt new file mode 100644 index 0000000000..a9af58a25a --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageColumns.kt @@ -0,0 +1,24 @@ +package com.fsck.k9.mailstore + +object MessageColumns { + const val ID = "id" + const val UID = "uid" + const val INTERNAL_DATE = "internal_date" + const val SUBJECT = "subject" + const val DATE = "date" + const val MESSAGE_ID = "message_id" + const val SENDER_LIST = "sender_list" + const val TO_LIST = "to_list" + const val CC_LIST = "cc_list" + const val BCC_LIST = "bcc_list" + const val REPLY_TO_LIST = "reply_to_list" + const val FLAGS = "flags" + const val ATTACHMENT_COUNT = "attachment_count" + const val FOLDER_ID = "folder_id" + const val PREVIEW_TYPE = "preview_type" + const val PREVIEW = "preview" + const val READ = "read" + const val FLAGGED = "flagged" + const val ANSWERED = "answered" + const val FORWARDED = "forwarded" +} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt index 2e4718d408..16be0694ae 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt @@ -6,8 +6,14 @@ import org.koin.dsl.module val messageListUiModule = module { viewModel { MessageListViewModel(get()) } factory { DefaultFolderProvider() } - factory { MessageListExtractor(get(), get()) } - factory { MessageListLoader(get(), get(), get(), get()) } + factory { + MessageListLoader( + preferences = get(), + localStoreProvider = get(), + messageListRepository = get(), + messageHelper = get() + ) + } factory { MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get()) } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt deleted file mode 100644 index ec4d46865e..0000000000 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListExtractor.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.fsck.k9.ui.messagelist - -import android.database.Cursor -import com.fsck.k9.Preferences -import com.fsck.k9.fragment.MLFProjectionInfo -import com.fsck.k9.helper.MessageHelper -import com.fsck.k9.helper.map -import com.fsck.k9.mail.Address -import com.fsck.k9.mailstore.DatabasePreviewType -import com.fsck.k9.ui.helper.DisplayAddressHelper - -class MessageListExtractor( - private val preferences: Preferences, - private val messageHelper: MessageHelper -) { - fun extractMessageList(cursor: Cursor, uniqueIdColumn: Int, threadCountIncluded: Boolean): List { - return cursor.map { extractMessageListItem(it, uniqueIdColumn, threadCountIncluded) } - } - - private fun extractMessageListItem( - cursor: Cursor, - uniqueIdColumn: Int, - threadCountIncluded: Boolean - ): MessageListItem { - val accountUuid = cursor.getString(MLFProjectionInfo.ACCOUNT_UUID_COLUMN) - val account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found") - - val fromList = cursor.getString(MLFProjectionInfo.SENDER_LIST_COLUMN) - val toList = cursor.getString(MLFProjectionInfo.TO_LIST_COLUMN) - val ccList = cursor.getString(MLFProjectionInfo.CC_LIST_COLUMN) - val fromAddresses = Address.unpack(fromList) - val toAddresses = Address.unpack(toList) - val ccAddresses = Address.unpack(ccList) - val toMe = messageHelper.toMe(account, toAddresses) - val ccMe = messageHelper.toMe(account, ccAddresses) - val messageDate = cursor.getLong(MLFProjectionInfo.DATE_COLUMN) - val threadCount = if (threadCountIncluded) cursor.getInt(MLFProjectionInfo.THREAD_COUNT_COLUMN) else 0 - val subject = cursor.getString(MLFProjectionInfo.SUBJECT_COLUMN) - val isRead = cursor.getBoolean(MLFProjectionInfo.READ_COLUMN) - val isStarred = cursor.getBoolean(MLFProjectionInfo.FLAGGED_COLUMN) - val isAnswered = cursor.getBoolean(MLFProjectionInfo.ANSWERED_COLUMN) - val isForwarded = cursor.getBoolean(MLFProjectionInfo.FORWARDED_COLUMN) - val hasAttachments = cursor.getInt(MLFProjectionInfo.ATTACHMENT_COUNT_COLUMN) > 0 - val previewTypeString = cursor.getString(MLFProjectionInfo.PREVIEW_TYPE_COLUMN) - val previewType = DatabasePreviewType.fromDatabaseValue(previewTypeString) - val isMessageEncrypted = previewType == DatabasePreviewType.ENCRYPTED - val previewText = getPreviewText(previewType, cursor) - val uniqueId = cursor.getLong(uniqueIdColumn) - val folderId = cursor.getLong(MLFProjectionInfo.FOLDER_ID_COLUMN) - val messageUid = cursor.getString(MLFProjectionInfo.UID_COLUMN) - val databaseId = cursor.getLong(MLFProjectionInfo.ID_COLUMN) - val threadRoot = cursor.getLong(MLFProjectionInfo.THREAD_ROOT_COLUMN) - val showRecipients = DisplayAddressHelper.shouldShowRecipients(account, folderId) - val displayAddress = if (showRecipients) toAddresses.firstOrNull() else fromAddresses.firstOrNull() - val displayName = if (showRecipients) { - messageHelper.getRecipientDisplayNames(toAddresses) - } else { - messageHelper.getSenderDisplayName(displayAddress) - } - - return MessageListItem( - account, - subject, - threadCount, - messageDate, - displayName, - displayAddress, - toMe, - ccMe, - previewText, - isMessageEncrypted, - isRead, - isStarred, - isAnswered, - isForwarded, - hasAttachments, - uniqueId, - folderId, - messageUid, - databaseId, - threadRoot - ) - } - - private fun getPreviewText(previewType: DatabasePreviewType?, cursor: Cursor): String { - return if (previewType == DatabasePreviewType.TEXT) { - cursor.getString(MLFProjectionInfo.PREVIEW_COLUMN) ?: "" - } else { - "" - } - } - - private fun Cursor.getBoolean(columnIndex: Int): Boolean = getInt(columnIndex) == 1 -} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt index ea2df9a054..706225021d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt @@ -8,6 +8,7 @@ data class MessageListItem( val subject: String?, val threadCount: Int, val messageDate: Long, + val internalDate: Long, val displayName: CharSequence, val displayAddress: Address?, val toMe: Boolean, diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemMapper.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemMapper.kt new file mode 100644 index 0000000000..5840804cdd --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemMapper.kt @@ -0,0 +1,60 @@ +package com.fsck.k9.ui.messagelist + +import com.fsck.k9.Account +import com.fsck.k9.helper.MessageHelper +import com.fsck.k9.mailstore.MessageDetailsAccessor +import com.fsck.k9.mailstore.MessageMapper +import com.fsck.k9.message.extractors.PreviewResult.PreviewType +import com.fsck.k9.ui.helper.DisplayAddressHelper + +class MessageListItemMapper( + private val messageHelper: MessageHelper, + private val account: Account +) : MessageMapper { + + override fun map(message: MessageDetailsAccessor): MessageListItem { + val fromAddresses = message.fromAddresses + val toAddresses = message.toAddresses + val toMe = messageHelper.toMe(account, toAddresses) + val ccMe = messageHelper.toMe(account, message.ccAddresses) + val previewResult = message.preview + val isMessageEncrypted = previewResult.previewType == PreviewType.ENCRYPTED + val previewText = if (previewResult.isPreviewTextAvailable) previewResult.previewText else "" + val uniqueId = createUniqueId(account, message.id) + val showRecipients = DisplayAddressHelper.shouldShowRecipients(account, message.folderId) + val displayAddress = if (showRecipients) toAddresses.firstOrNull() else fromAddresses.firstOrNull() + val displayName = if (showRecipients) { + messageHelper.getRecipientDisplayNames(toAddresses.toTypedArray()) + } else { + messageHelper.getSenderDisplayName(displayAddress) + } + + return MessageListItem( + account, + message.subject, + message.threadCount, + message.messageDate, + message.internalDate, + displayName, + displayAddress, + toMe, + ccMe, + previewText, + isMessageEncrypted, + message.isRead, + message.isStarred, + message.isAnswered, + message.isForwarded, + message.hasAttachments, + uniqueId, + message.folderId, + message.messageServerId, + message.id, + message.threadRoot + ) + } + + private fun createUniqueId(account: Account, messageId: Long): Long { + return ((account.accountNumber + 1).toLong() shl 52) + messageId + } +} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt index 7a5b6806b6..59dcdd2b3c 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt @@ -1,42 +1,23 @@ package com.fsck.k9.ui.messagelist -import android.annotation.SuppressLint -import android.content.ContentResolver -import android.database.Cursor -import android.database.MatrixCursor -import android.database.sqlite.SQLiteException -import android.net.Uri import com.fsck.k9.Account import com.fsck.k9.Account.SortType -import com.fsck.k9.K9 import com.fsck.k9.Preferences -import com.fsck.k9.fragment.MLFProjectionInfo -import com.fsck.k9.fragment.MessageListFragmentComparators.ArrivalComparator -import com.fsck.k9.fragment.MessageListFragmentComparators.AttachmentComparator -import com.fsck.k9.fragment.MessageListFragmentComparators.ComparatorChain -import com.fsck.k9.fragment.MessageListFragmentComparators.DateComparator -import com.fsck.k9.fragment.MessageListFragmentComparators.FlaggedComparator -import com.fsck.k9.fragment.MessageListFragmentComparators.ReverseComparator -import com.fsck.k9.fragment.MessageListFragmentComparators.ReverseIdComparator -import com.fsck.k9.fragment.MessageListFragmentComparators.SenderComparator -import com.fsck.k9.fragment.MessageListFragmentComparators.SubjectComparator -import com.fsck.k9.fragment.MessageListFragmentComparators.UnreadComparator -import com.fsck.k9.helper.MergeCursorWithUniqueId +import com.fsck.k9.helper.MessageHelper import com.fsck.k9.mailstore.LocalStoreProvider -import com.fsck.k9.provider.EmailProvider +import com.fsck.k9.mailstore.MessageColumns +import com.fsck.k9.mailstore.MessageListRepository import com.fsck.k9.search.LocalSearch import com.fsck.k9.search.SearchSpecification.SearchField import com.fsck.k9.search.SqlQueryBuilder import com.fsck.k9.search.getAccounts -import java.util.ArrayList -import java.util.Comparator import timber.log.Timber class MessageListLoader( private val preferences: Preferences, - private val contentResolver: ContentResolver, private val localStoreProvider: LocalStoreProvider, - private val messageListExtractor: MessageListExtractor + private val messageListRepository: MessageListRepository, + private val messageHelper: MessageHelper ) { fun getMessageList(config: MessageListConfig): MessageListInfo { @@ -52,113 +33,76 @@ class MessageListLoader( private fun getMessageListInfo(config: MessageListConfig): MessageListInfo { val accounts = config.search.getAccounts(preferences) - val cursors = accounts - .mapNotNull { loadMessageListForAccount(it, config) } - .toTypedArray() - - if (cursors.isEmpty()) { - Timber.w("Couldn't get message list") - return MessageListInfo(messageListItems = emptyList(), hasMoreMessages = false) - } - - val cursor: Cursor - val uniqueIdColumn: Int - if (cursors.size == 1) { - cursor = cursors[0] - uniqueIdColumn = MLFProjectionInfo.ID_COLUMN - } else { - cursor = MergeCursorWithUniqueId(cursors, getComparator(config)) - uniqueIdColumn = cursor.getColumnIndex("_id") - } + val messageListItems = accounts + .flatMap { account -> + loadMessageListForAccount(account, config) + } + .sortedWith(config) - val messageListItems = cursor.use { - messageListExtractor.extractMessageList( - cursor, - uniqueIdColumn, - threadCountIncluded = config.showingThreadedList - ) - } val hasMoreMessages = loadHasMoreMessages(accounts, config.search.folderIds) return MessageListInfo(messageListItems, hasMoreMessages) } - @SuppressLint("Recycle") - private fun loadMessageListForAccount(account: Account, config: MessageListConfig): Cursor? { + private fun loadMessageListForAccount(account: Account, config: MessageListConfig): List { val accountUuid = account.uuid - val threadId: String? = getThreadId(config.search) + val threadId = getThreadId(config.search) + val sortOrder = buildSortOrder(config) + val mapper = MessageListItemMapper(messageHelper, account) - val uri: Uri - val projection: Array - val needConditions: Boolean - when { + return when { threadId != null -> { - uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/$accountUuid/thread/$threadId") - projection = MLFProjectionInfo.PROJECTION - needConditions = false + messageListRepository.getThread(accountUuid, threadId, sortOrder, mapper) } config.showingThreadedList -> { - uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/$accountUuid/messages/threaded") - projection = MLFProjectionInfo.THREADED_PROJECTION - needConditions = true + val (selection, selectionArgs) = buildSelection(account, config) + messageListRepository.getThreadedMessages(accountUuid, selection, selectionArgs, sortOrder, mapper) } else -> { - uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/$accountUuid/messages") - projection = MLFProjectionInfo.PROJECTION - needConditions = true + val (selection, selectionArgs) = buildSelection(account, config) + messageListRepository.getMessages(accountUuid, selection, selectionArgs, sortOrder, mapper) } } + } + private fun buildSelection(account: Account, config: MessageListConfig): Pair> { val query = StringBuilder() - val queryArgs: MutableList = ArrayList() - if (needConditions) { - val activeMessage = config.activeMessage - val selectActive = activeMessage != null && activeMessage.accountUuid == accountUuid - if (selectActive && activeMessage != null) { - query.append("(${EmailProvider.MessageColumns.UID} = ? AND ${EmailProvider.MessageColumns.FOLDER_ID} = ?) OR (") - queryArgs.add(activeMessage.uid) - queryArgs.add(activeMessage.folderId.toString()) - } + val queryArgs = mutableListOf() + + val activeMessage = config.activeMessage + val selectActive = activeMessage != null && activeMessage.accountUuid == account.uuid + if (selectActive && activeMessage != null) { + query.append("(${MessageColumns.UID} = ? AND ${MessageColumns.FOLDER_ID} = ?) OR (") + queryArgs.add(activeMessage.uid) + queryArgs.add(activeMessage.folderId.toString()) + } - SqlQueryBuilder.buildWhereClause(account, config.search.conditions, query, queryArgs) + SqlQueryBuilder.buildWhereClause(account, config.search.conditions, query, queryArgs) - if (selectActive) { - query.append(')') - } + if (selectActive) { + query.append(')') } val selection = query.toString() val selectionArgs = queryArgs.toTypedArray() - val sortOrder: String = buildSortOrder(config) - - return try { - contentResolver.query(uri, projection, selection, selectionArgs, sortOrder) - } catch (e: SQLiteException) { - Timber.e(e, "Error querying EmailProvider") - - if (K9.DEVELOPER_MODE && e.message?.contains("malformed MATCH expression") == false) { - throw e - } - - return MatrixCursor(projection) - } + return selection to selectionArgs } - private fun getThreadId(search: LocalSearch): String? { - return search.leafSet.firstOrNull { it.condition.field == SearchField.THREAD_ID }?.condition?.value + private fun getThreadId(search: LocalSearch): Long? { + return search.leafSet.firstOrNull { it.condition.field == SearchField.THREAD_ID }?.condition?.value?.toLong() } private fun buildSortOrder(config: MessageListConfig): String { val sortColumn = when (config.sortType) { - SortType.SORT_ARRIVAL -> EmailProvider.MessageColumns.INTERNAL_DATE - SortType.SORT_ATTACHMENT -> "(${EmailProvider.MessageColumns.ATTACHMENT_COUNT} < 1)" - SortType.SORT_FLAGGED -> "(${EmailProvider.MessageColumns.FLAGGED} != 1)" - SortType.SORT_SENDER -> EmailProvider.MessageColumns.SENDER_LIST // FIXME - SortType.SORT_SUBJECT -> "${EmailProvider.MessageColumns.SUBJECT} COLLATE NOCASE" - SortType.SORT_UNREAD -> EmailProvider.MessageColumns.READ - SortType.SORT_DATE -> EmailProvider.MessageColumns.DATE - else -> EmailProvider.MessageColumns.DATE + SortType.SORT_ARRIVAL -> MessageColumns.INTERNAL_DATE + SortType.SORT_ATTACHMENT -> "(${MessageColumns.ATTACHMENT_COUNT} < 1)" + SortType.SORT_FLAGGED -> "(${MessageColumns.FLAGGED} != 1)" + SortType.SORT_SENDER -> MessageColumns.SENDER_LIST // FIXME + SortType.SORT_SUBJECT -> "${MessageColumns.SUBJECT} COLLATE NOCASE" + SortType.SORT_UNREAD -> MessageColumns.READ + SortType.SORT_DATE -> MessageColumns.DATE + else -> MessageColumns.DATE } val sortDirection = if (config.sortAscending) " ASC" else " DESC" @@ -166,41 +110,46 @@ class MessageListLoader( "" } else { if (config.sortDateAscending) { - "${EmailProvider.MessageColumns.DATE} ASC, " + "${MessageColumns.DATE} ASC, " } else { - "${EmailProvider.MessageColumns.DATE} DESC, " + "${MessageColumns.DATE} DESC, " } } - return "$sortColumn$sortDirection, $secondarySort${EmailProvider.MessageColumns.ID} DESC" + return "$sortColumn$sortDirection, $secondarySort${MessageColumns.ID} DESC" } - private fun getComparator(config: MessageListConfig): Comparator? { - val chain: MutableList> = ArrayList(3 /* we add 3 comparators at most */) - - // Add the specified comparator - val comparator = SORT_COMPARATORS.getValue(config.sortType) - if (config.sortAscending) { - chain.add(comparator) - } else { - chain.add(ReverseComparator(comparator)) - } - - // Add the date comparator if not already specified - if (config.sortType != SortType.SORT_DATE && config.sortType != SortType.SORT_ARRIVAL) { - val dateComparator = SORT_COMPARATORS.getValue(SortType.SORT_DATE) - if (config.sortDateAscending) { - chain.add(dateComparator) - } else { - chain.add(ReverseComparator(dateComparator)) + private fun List.sortedWith(config: MessageListConfig): List { + val comparator = when (config.sortType) { + SortType.SORT_DATE -> { + compareBy(config.sortAscending) { it.messageDate } } - } - - // Add the id comparator - chain.add(ReverseIdComparator()) + SortType.SORT_ARRIVAL -> { + compareBy(config.sortAscending) { it.internalDate } + } + SortType.SORT_SUBJECT -> { + compareStringBy(config.sortAscending) { it.subject.orEmpty() } + .thenByDate(config) + } + SortType.SORT_SENDER -> { + compareStringBy(config.sortAscending) { it.displayName.toString() } + .thenByDate(config) + } + SortType.SORT_UNREAD -> { + compareBy(config.sortAscending) { it.isRead } + .thenByDate(config) + } + SortType.SORT_FLAGGED -> { + compareBy(!config.sortAscending) { it.isStarred } + .thenByDate(config) + } + SortType.SORT_ATTACHMENT -> { + compareBy(!config.sortAscending) { it.hasAttachments } + .thenByDate(config) + } + }.thenByDescending { it.databaseId } - // Build the comparator chain - return ComparatorChain(chain) + return this.sortedWith(comparator) } private fun loadHasMoreMessages(accounts: List, folderIds: List): Boolean { @@ -215,17 +164,29 @@ class MessageListLoader( false } } +} + +private inline fun compareBy(sortAscending: Boolean, crossinline selector: (T) -> Comparable<*>?): Comparator { + return if (sortAscending) { + compareBy(selector) + } else { + compareByDescending(selector) + } +} + +private inline fun compareStringBy(sortAscending: Boolean, crossinline selector: (T) -> String): Comparator { + return if (sortAscending) { + compareBy(String.CASE_INSENSITIVE_ORDER, selector) + } else { + compareByDescending(String.CASE_INSENSITIVE_ORDER, selector) + } +} - companion object { - private val SORT_COMPARATORS = mapOf( - SortType.SORT_ATTACHMENT to AttachmentComparator(), - SortType.SORT_DATE to DateComparator(), - SortType.SORT_ARRIVAL to ArrivalComparator(), - SortType.SORT_FLAGGED to FlaggedComparator(), - SortType.SORT_SUBJECT to SubjectComparator(), - SortType.SORT_SENDER to SenderComparator(), - SortType.SORT_UNREAD to UnreadComparator() - ) +private fun Comparator.thenByDate(config: MessageListConfig): Comparator { + return if (config.sortDateAscending) { + thenBy { it.messageDate } + } else { + thenByDescending { it.messageDate } } } diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt index cdbacdb2ce..168f2bc688 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt @@ -473,6 +473,7 @@ class MessageListAdapterTest : RobolectricTest() { subject: String? = "irrelevant", threadCount: Int = 0, messageDate: Long = 0L, + internalDate: Long = 0L, displayName: CharSequence = "irrelevant", displayAddress: Address? = Address.parse("irrelevant@domain.example").first(), toMe: Boolean = false, @@ -495,6 +496,7 @@ class MessageListAdapterTest : RobolectricTest() { subject, threadCount, messageDate, + internalDate, displayName, displayAddress, toMe, -- GitLab From 7ea928bba595556b75fbfe15f938a26e759158d8 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 16:52:02 +0200 Subject: [PATCH 51/85] Finally get rid of `EmailProvider` --- .../k9/cache/EmailProviderCacheCursor.java | 144 ---- .../java/com/fsck/k9/helper/MergeCursor.java | 462 ----------- .../k9/helper/MergeCursorWithUniqueId.java | 83 -- .../com/fsck/k9/mailstore/LocalStore.java | 2 - .../com/fsck/k9/provider/EmailProvider.java | 719 ------------------ .../fsck/k9/mailstore/K9BackendFolderTest.kt | 9 - .../fsck/k9/mailstore/K9BackendStorageTest.kt | 9 - app/k9mail/src/main/AndroidManifest.xml | 5 - .../fsck/k9/fragment/MLFProjectionInfo.java | 60 -- .../MessageListFragmentComparators.java | 163 ---- .../java/com/fsck/k9/fragment/MlfUtils.java | 10 - 11 files changed, 1666 deletions(-) delete mode 100644 app/core/src/main/java/com/fsck/k9/cache/EmailProviderCacheCursor.java delete mode 100644 app/core/src/main/java/com/fsck/k9/helper/MergeCursor.java delete mode 100644 app/core/src/main/java/com/fsck/k9/helper/MergeCursorWithUniqueId.java delete mode 100644 app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java delete mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/fragment/MLFProjectionInfo.java delete mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragmentComparators.java diff --git a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCacheCursor.java b/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCacheCursor.java deleted file mode 100644 index 7de6177e86..0000000000 --- a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCacheCursor.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.fsck.k9.cache; - -import java.util.ArrayList; -import java.util.List; - -import com.fsck.k9.provider.EmailProvider.MessageColumns; -import com.fsck.k9.provider.EmailProvider.ThreadColumns; - -import android.content.Context; -import android.database.Cursor; -import android.database.CursorWrapper; - -/** - * A {@link CursorWrapper} that utilizes {@link EmailProviderCache}. - */ -public class EmailProviderCacheCursor extends CursorWrapper { - private EmailProviderCache mCache; - private List mHiddenRows = new ArrayList<>(); - private int mMessageIdColumn; - private int mFolderIdColumn; - private int mThreadRootColumn; - - /** - * The cursor's current position. - * - * Note: This is only used when {@link #mHiddenRows} isn't empty. - */ - private int mPosition; - - - public EmailProviderCacheCursor(String accountUuid, Cursor cursor) { - super(cursor); - - mCache = EmailProviderCache.getCache(accountUuid); - - mMessageIdColumn = cursor.getColumnIndex(MessageColumns.ID); - mFolderIdColumn = cursor.getColumnIndex(MessageColumns.FOLDER_ID); - mThreadRootColumn = cursor.getColumnIndex(ThreadColumns.ROOT); - - if (mMessageIdColumn == -1 || mFolderIdColumn == -1 || mThreadRootColumn == -1) { - throw new IllegalArgumentException("The supplied cursor needs to contain the " + - "following columns: " + MessageColumns.ID + ", " + MessageColumns.FOLDER_ID + - ", " + ThreadColumns.ROOT); - } - - while (cursor.moveToNext()) { - long messageId = cursor.getLong(mMessageIdColumn); - long folderId = cursor.getLong(mFolderIdColumn); - if (mCache.isMessageHidden(messageId, folderId)) { - mHiddenRows.add(cursor.getPosition()); - } - } - - // Reset the cursor position - cursor.moveToFirst(); - cursor.moveToPrevious(); - } - - @Override - public int getInt(int columnIndex) { - long messageId = getLong(mMessageIdColumn); - long threadRootId = getLong(mThreadRootColumn); - - String columnName = getColumnName(columnIndex); - String value = mCache.getValueForMessage(messageId, columnName); - - if (value != null) { - return Integer.parseInt(value); - } - - value = mCache.getValueForThread(threadRootId, columnName); - if (value != null) { - return Integer.parseInt(value); - } - - return super.getInt(columnIndex); - } - - @Override - public int getCount() { - return super.getCount() - mHiddenRows.size(); - } - - @Override - public boolean moveToFirst() { - return moveToPosition(0); - } - - @Override - public boolean moveToLast() { - return moveToPosition(getCount()); - } - - @Override - public boolean moveToNext() { - return moveToPosition(getPosition() + 1); - } - - @Override - public boolean moveToPrevious() { - return moveToPosition(getPosition() - 1); - } - - @Override - public boolean move(int offset) { - return moveToPosition(getPosition() + offset); - } - - @Override - public boolean moveToPosition(int position) { - if (mHiddenRows.isEmpty()) { - return super.moveToPosition(position); - } - - mPosition = position; - int newPosition = position; - for (int hiddenRow : mHiddenRows) { - if (hiddenRow > newPosition) { - break; - } - newPosition++; - } - - return super.moveToPosition(newPosition); - } - - @Override - public int getPosition() { - if (mHiddenRows.isEmpty()) { - return super.getPosition(); - } - - return mPosition; - } - - @Override - public boolean isLast() { - if (mHiddenRows.isEmpty()) { - return super.isLast(); - } - - return (mPosition == getCount() - 1); - } -} diff --git a/app/core/src/main/java/com/fsck/k9/helper/MergeCursor.java b/app/core/src/main/java/com/fsck/k9/helper/MergeCursor.java deleted file mode 100644 index f0424f9568..0000000000 --- a/app/core/src/main/java/com/fsck/k9/helper/MergeCursor.java +++ /dev/null @@ -1,462 +0,0 @@ -/* - * Copyright (C) 2012 The K-9 Dog Walkers - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.fsck.k9.helper; - -import java.util.Comparator; - -import android.annotation.TargetApi; -import android.content.ContentResolver; -import android.database.CharArrayBuffer; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.DataSetObserver; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; - - -/** - * This class can be used to combine multiple {@link Cursor}s into one. - */ -public class MergeCursor implements Cursor { - /** - * List of the cursors combined in this object. - */ - protected final Cursor[] mCursors; - - /** - * The currently active cursor. - */ - protected Cursor mActiveCursor; - - /** - * The index of the currently active cursor in {@link #mCursors}. - * - * @see #mActiveCursor - */ - protected int mActiveCursorIndex; - - /** - * The cursor's current position. - */ - protected int mPosition; - - /** - * Used to cache the value of {@link #getCount()}. - */ - private int mCount = -1; - - /** - * The comparator that is used to decide how the individual cursors are merged. - */ - private final Comparator mComparator; - - - /** - * Constructor - * - * @param cursors - * The list of cursors this {@code MultiCursor} should combine. - * @param comparator - * A comparator that is used to decide in what order the individual cursors are merged. - */ - public MergeCursor(Cursor[] cursors, Comparator comparator) { - mCursors = cursors.clone(); - mComparator = comparator; - - resetCursors(); - } - - private void resetCursors() { - mActiveCursorIndex = -1; - mActiveCursor = null; - mPosition = -1; - - for (int i = 0, len = mCursors.length; i < len; i++) { - Cursor cursor = mCursors[i]; - if (cursor != null) { - cursor.moveToPosition(-1); - - if (mActiveCursor == null) { - mActiveCursorIndex = i; - mActiveCursor = mCursors[mActiveCursorIndex]; - } - } - } - } - - @Override - public void close() { - for (Cursor cursor : mCursors) { - if (cursor != null) { - cursor.close(); - } - } - } - - @Override - public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { - mActiveCursor.copyStringToBuffer(columnIndex, buffer); - } - - @Override - public void deactivate() { - for (Cursor cursor : mCursors) { - if (cursor != null) { - cursor.deactivate(); - } - } - } - - @Override - public byte[] getBlob(int columnIndex) { - return mActiveCursor.getBlob(columnIndex); - } - - @Override - public int getColumnCount() { - return mActiveCursor.getColumnCount(); - } - - @Override - public int getColumnIndex(String columnName) { - return mActiveCursor.getColumnIndex(columnName); - } - - @Override - public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { - return mActiveCursor.getColumnIndexOrThrow(columnName); - } - - @Override - public String getColumnName(int columnIndex) { - return mActiveCursor.getColumnName(columnIndex); - } - - @Override - public String[] getColumnNames() { - return mActiveCursor.getColumnNames(); - } - - @Override - public int getCount() { - // CursorLoaders seem to call getCount() a lot. So we're caching the aggregated count. - if (mCount == -1) { - int count = 0; - for (Cursor cursor : mCursors) { - if (cursor != null) { - count += cursor.getCount(); - } - } - - mCount = count; - } - - return mCount; - } - - @Override - public double getDouble(int columnIndex) { - return mActiveCursor.getDouble(columnIndex); - } - - @Override - public float getFloat(int columnIndex) { - return mActiveCursor.getFloat(columnIndex); - } - - @Override - public int getInt(int columnIndex) { - return mActiveCursor.getInt(columnIndex); - } - - @Override - public long getLong(int columnIndex) { - return mActiveCursor.getLong(columnIndex); - } - - @Override - public int getPosition() { - return mPosition; - } - - @Override - public short getShort(int columnIndex) { - return mActiveCursor.getShort(columnIndex); - } - - @Override - public String getString(int columnIndex) { - return mActiveCursor.getString(columnIndex); - } - - @Override - public int getType(int columnIndex) { - return mActiveCursor.getType(columnIndex); - } - - @Override - public boolean getWantsAllOnMoveCalls() { - return mActiveCursor.getWantsAllOnMoveCalls(); - } - - @TargetApi(Build.VERSION_CODES.M) - @Override - public void setExtras(Bundle extras) { - mActiveCursor.setExtras(extras); - } - - @Override - public boolean isAfterLast() { - int count = getCount(); - return count == 0 || mPosition == count; - - } - - @Override - public boolean isBeforeFirst() { - return getCount() == 0 || mPosition == -1; - - } - - @Override - public boolean isClosed() { - return mActiveCursor.isClosed(); - } - - @Override - public boolean isFirst() { - return getCount() != 0 && mPosition == 0; - - } - - @Override - public boolean isLast() { - int count = getCount(); - return count != 0 && mPosition == count - 1; - - } - - @Override - public boolean isNull(int columnIndex) { - return mActiveCursor.isNull(columnIndex); - } - - @Override - public boolean move(int offset) { - return moveToPosition(mPosition + offset); - } - - @Override - public boolean moveToFirst() { - return moveToPosition(0); - } - - @Override - public boolean moveToLast() { - return moveToPosition(getCount() - 1); - } - - @Override - public boolean moveToNext() { - int count = getCount(); - if (mPosition == count) { - return false; - } - - if (mPosition == count - 1) { - mActiveCursor.moveToNext(); - mPosition++; - return false; - } - - int smallest = -1; - for (int i = 0, len = mCursors.length; i < len; i++) { - if (mCursors[i] == null || mCursors[i].getCount() == 0 || mCursors[i].isLast()) { - continue; - } - - if (smallest == -1) { - smallest = i; - mCursors[smallest].moveToNext(); - continue; - } - - Cursor left = mCursors[smallest]; - Cursor right = mCursors[i]; - - right.moveToNext(); - - int result = mComparator.compare(left, right); - if (result > 0) { - smallest = i; - left.moveToPrevious(); - } else { - right.moveToPrevious(); - } - } - - mPosition++; - if (smallest != -1) { - mActiveCursorIndex = smallest; - mActiveCursor = mCursors[mActiveCursorIndex]; - } - - return true; - } - - @Override - public boolean moveToPosition(int position) { - // Make sure position isn't past the end of the cursor - final int count = getCount(); - if (position >= count) { - mPosition = count; - return false; - } - - // Make sure position isn't before the beginning of the cursor - if (position < 0) { - mPosition = -1; - return false; - } - - // Check for no-op moves, and skip the rest of the work for them - if (position == mPosition) { - return true; - } - - if (position > mPosition) { - for (int i = 0, end = position - mPosition; i < end; i++) { - if (!moveToNext()) { - return false; - } - } - } else { - for (int i = 0, end = mPosition - position; i < end; i++) { - if (!moveToPrevious()) { - return false; - } - } - } - - return true; - } - - @Override - public boolean moveToPrevious() { - if (mPosition < 0) { - return false; - } - - mActiveCursor.moveToPrevious(); - - if (mPosition == 0) { - mPosition = -1; - return false; - } - - int greatest = -1; - for (int i = 0, len = mCursors.length; i < len; i++) { - if (mCursors[i] == null || mCursors[i].isBeforeFirst()) { - continue; - } - - if (greatest == -1) { - greatest = i; - continue; - } - - Cursor left = mCursors[greatest]; - Cursor right = mCursors[i]; - - int result = mComparator.compare(left, right); - if (result <= 0) { - greatest = i; - } - } - - mPosition--; - if (greatest != -1) { - mActiveCursorIndex = greatest; - mActiveCursor = mCursors[mActiveCursorIndex]; - } - - return true; - } - - @Override - public void registerContentObserver(ContentObserver observer) { - for (Cursor cursor : mCursors) { - cursor.registerContentObserver(observer); - } - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - for (Cursor cursor : mCursors) { - cursor.registerDataSetObserver(observer); - } - } - - @Deprecated - @Override - public boolean requery() { - boolean success = true; - for (Cursor cursor : mCursors) { - success &= cursor.requery(); - } - - return success; - } - - @Override - public void setNotificationUri(ContentResolver cr, Uri uri) { - for (Cursor cursor : mCursors) { - cursor.setNotificationUri(cr, uri); - } - } - - @Override - public void unregisterContentObserver(ContentObserver observer) { - for (Cursor cursor : mCursors) { - cursor.unregisterContentObserver(observer); - } - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - for (Cursor cursor : mCursors) { - cursor.unregisterDataSetObserver(observer); - } - } - - @Override - public Bundle getExtras() { - throw new RuntimeException("Not implemented"); - } - - @Override - public Bundle respond(Bundle extras) { - throw new RuntimeException("Not implemented"); - } - - @Override - public Uri getNotificationUri() { - return null; - } -} diff --git a/app/core/src/main/java/com/fsck/k9/helper/MergeCursorWithUniqueId.java b/app/core/src/main/java/com/fsck/k9/helper/MergeCursorWithUniqueId.java deleted file mode 100644 index 111d49fbbb..0000000000 --- a/app/core/src/main/java/com/fsck/k9/helper/MergeCursorWithUniqueId.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.fsck.k9.helper; - -import java.util.Comparator; - -import android.database.Cursor; - - -public class MergeCursorWithUniqueId extends MergeCursor { - private static final int SHIFT = 48; - private static final long MAX_ID = (1L << SHIFT) - 1; - private static final long MAX_CURSORS = 1L << (63 - SHIFT); - - private int mColumnCount = -1; - private int mIdColumnIndex = -1; - - - public MergeCursorWithUniqueId(Cursor[] cursors, Comparator comparator) { - super(cursors, comparator); - - if (cursors.length > MAX_CURSORS) { - throw new IllegalArgumentException("This class only supports up to " + - MAX_CURSORS + " cursors"); - } - } - - @Override - public int getColumnCount() { - if (mColumnCount == -1) { - mColumnCount = super.getColumnCount(); - } - - return mColumnCount + 1; - } - - @Override - public int getColumnIndex(String columnName) { - if ("_id".equals(columnName)) { - return getUniqueIdColumnIndex(); - } - - return super.getColumnIndexOrThrow(columnName); - } - - @Override - public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { - if ("_id".equals(columnName)) { - return getUniqueIdColumnIndex(); - } - - return super.getColumnIndexOrThrow(columnName); - } - - @Override - public long getLong(int columnIndex) { - if (columnIndex == getUniqueIdColumnIndex()) { - long id = getPerCursorId(); - if (id > MAX_ID) { - throw new RuntimeException("Sorry, " + this.getClass().getName() + - " can only handle '_id' values up to " + SHIFT + " bits."); - } - - return (((long) mActiveCursorIndex) << SHIFT) + id; - } - - return super.getLong(columnIndex); - } - - protected int getUniqueIdColumnIndex() { - if (mColumnCount == -1) { - mColumnCount = super.getColumnCount(); - } - - return mColumnCount; - } - - protected long getPerCursorId() { - if (mIdColumnIndex == -1) { - mIdColumnIndex = super.getColumnIndexOrThrow("_id"); - } - - return super.getLong(mIdColumnIndex); - } -} diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java index a502008e87..3f846a6c6c 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -18,7 +18,6 @@ import java.util.Locale; import java.util.Map; import java.util.Stack; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -50,7 +49,6 @@ import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.SchemaDefinition; import com.fsck.k9.mailstore.StorageManager.InternalStorageProvider; import com.fsck.k9.message.extractors.AttachmentInfoExtractor; -import com.fsck.k9.provider.EmailProvider.MessageColumns; import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.SearchField; diff --git a/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java b/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java deleted file mode 100644 index 35388b3ef4..0000000000 --- a/app/core/src/main/java/com/fsck/k9/provider/EmailProvider.java +++ /dev/null @@ -1,719 +0,0 @@ -package com.fsck.k9.provider; - - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import android.content.ContentProvider; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.CursorWrapper; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.text.TextUtils; - -import com.fsck.k9.Account; -import com.fsck.k9.DI; -import com.fsck.k9.Preferences; -import com.fsck.k9.cache.EmailProviderCacheCursor; -import com.fsck.k9.helper.Utility; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mailstore.LocalStore; -import com.fsck.k9.mailstore.LocalStoreProvider; -import com.fsck.k9.mailstore.LockableDatabase; -import com.fsck.k9.mailstore.LockableDatabase.DbCallback; -import com.fsck.k9.search.SqlQueryBuilder; - - -/** - * Content Provider used to display the message list etc. - * - *

- * For now this content provider is for internal use only. In the future we may allow third-party - * apps to access K-9 Mail content using this content provider. - *

- */ -/* - * TODO: - * - add support for account list and folder list - */ -public class EmailProvider extends ContentProvider { - public static String AUTHORITY; - public static Uri CONTENT_URI; - - private static Uri getNotificationUri(String accountUuid) { - return Uri.withAppendedPath(CONTENT_URI, "account/" + accountUuid + "/messages"); - } - - private UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - - - /* - * Constants that are used for the URI matching. - */ - private static final int MESSAGE_BASE = 0; - private static final int MESSAGES = MESSAGE_BASE; - private static final int MESSAGES_THREADED = MESSAGE_BASE + 1; - private static final int MESSAGES_THREAD = MESSAGE_BASE + 2; - - - private static final String MESSAGES_TABLE = "messages"; - - private static final Map THREAD_AGGREGATION_FUNCS = new HashMap<>(); - static { - THREAD_AGGREGATION_FUNCS.put(MessageColumns.DATE, "MAX"); - THREAD_AGGREGATION_FUNCS.put(MessageColumns.INTERNAL_DATE, "MAX"); - THREAD_AGGREGATION_FUNCS.put(MessageColumns.ATTACHMENT_COUNT, "SUM"); - THREAD_AGGREGATION_FUNCS.put(MessageColumns.READ, "MIN"); - THREAD_AGGREGATION_FUNCS.put(MessageColumns.FLAGGED, "MAX"); - THREAD_AGGREGATION_FUNCS.put(MessageColumns.ANSWERED, "MIN"); - THREAD_AGGREGATION_FUNCS.put(MessageColumns.FORWARDED, "MIN"); - } - - private static final String[] FIXUP_MESSAGES_COLUMNS = { - MessageColumns.ID - }; - - private static final String[] FIXUP_AGGREGATED_MESSAGES_COLUMNS = { - MessageColumns.DATE, - MessageColumns.INTERNAL_DATE, - MessageColumns.ATTACHMENT_COUNT, - MessageColumns.READ, - MessageColumns.FLAGGED, - MessageColumns.ANSWERED, - MessageColumns.FORWARDED - }; - - private static final String FOLDERS_TABLE = "folders"; - - private static final String[] FOLDERS_COLUMNS = { - FolderColumns.ID, - FolderColumns.NAME, - FolderColumns.LAST_UPDATED, - FolderColumns.UNREAD_COUNT, - FolderColumns.VISIBLE_LIMIT, - FolderColumns.STATUS, - FolderColumns.PUSH_STATE, - FolderColumns.LAST_PUSHED, - FolderColumns.FLAGGED_COUNT, - FolderColumns.INTEGRATE, - FolderColumns.TOP_GROUP, - FolderColumns.POLL_CLASS, - FolderColumns.PUSH_CLASS, - FolderColumns.DISPLAY_CLASS, - FolderColumns.SERVER_ID - }; - - private static final String THREADS_TABLE = "threads"; - - public interface SpecialColumns { - String ACCOUNT_UUID = "account_uuid"; - - String THREAD_COUNT = "thread_count"; - - String FOLDER_SERVER_ID = "server_id"; - String INTEGRATE = "integrate"; - } - - public interface MessageColumns { - String ID = "id"; - String UID = "uid"; - String INTERNAL_DATE = "internal_date"; - String SUBJECT = "subject"; - String DATE = "date"; - String MESSAGE_ID = "message_id"; - String SENDER_LIST = "sender_list"; - String TO_LIST = "to_list"; - String CC_LIST = "cc_list"; - String BCC_LIST = "bcc_list"; - String REPLY_TO_LIST = "reply_to_list"; - String FLAGS = "flags"; - String ATTACHMENT_COUNT = "attachment_count"; - String FOLDER_ID = "folder_id"; - String PREVIEW_TYPE = "preview_type"; - String PREVIEW = "preview"; - String READ = "read"; - String FLAGGED = "flagged"; - String ANSWERED = "answered"; - String FORWARDED = "forwarded"; - } - - private interface InternalMessageColumns extends MessageColumns { - String DELETED = "deleted"; - String EMPTY = "empty"; - String MIME_TYPE = "mime_type"; - } - - public interface FolderColumns { - String ID = "id"; - String NAME = "name"; - String LAST_UPDATED = "last_updated"; - String UNREAD_COUNT = "unread_count"; - String VISIBLE_LIMIT = "visible_limit"; - String STATUS = "status"; - String PUSH_STATE = "push_state"; - String LAST_PUSHED = "last_pushed"; - String FLAGGED_COUNT = "flagged_count"; - String INTEGRATE = "integrate"; - String TOP_GROUP = "top_group"; - String POLL_CLASS = "poll_class"; - String PUSH_CLASS = "push_class"; - String DISPLAY_CLASS = "display_class"; - String SERVER_ID = "server_id"; - } - - public interface ThreadColumns { - String ID = "id"; - String MESSAGE_ID = "message_id"; - String ROOT = "root"; - String PARENT = "parent"; - } - - public interface StatsColumns { - String UNREAD_COUNT = "unread_count"; - String FLAGGED_COUNT = "flagged_count"; - } - - - private Preferences mPreferences; - - - @Override - public boolean onCreate() { - String packageName = getContext().getPackageName(); - AUTHORITY = packageName + ".provider.email"; - CONTENT_URI = Uri.parse("content://" + AUTHORITY); - - uriMatcher.addURI(AUTHORITY, "account/*/messages", MESSAGES); - uriMatcher.addURI(AUTHORITY, "account/*/messages/threaded", MESSAGES_THREADED); - uriMatcher.addURI(AUTHORITY, "account/*/thread/#", MESSAGES_THREAD); - - return true; - } - - @Override - public String getType(Uri uri) { - throw new RuntimeException("not implemented yet"); - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - int match = uriMatcher.match(uri); - if (match < 0) { - throw new IllegalArgumentException("Unknown URI: " + uri); - } - - ContentResolver contentResolver = getContext().getContentResolver(); - Cursor cursor = null; - switch (match) { - case MESSAGES: - case MESSAGES_THREADED: - case MESSAGES_THREAD: { - List segments = uri.getPathSegments(); - String accountUuid = segments.get(1); - - List dbColumnNames = new ArrayList<>(projection.length); - Map specialColumns = new HashMap<>(); - for (String columnName : projection) { - if (SpecialColumns.ACCOUNT_UUID.equals(columnName)) { - specialColumns.put(SpecialColumns.ACCOUNT_UUID, accountUuid); - } else { - dbColumnNames.add(columnName); - } - } - - String[] dbProjection = dbColumnNames.toArray(new String[0]); - - if (match == MESSAGES) { - cursor = getMessages(accountUuid, dbProjection, selection, selectionArgs, sortOrder); - } else if (match == MESSAGES_THREADED) { - cursor = getThreadedMessages(accountUuid, dbProjection, selection, selectionArgs, sortOrder); - } else if (match == MESSAGES_THREAD) { - String threadId = segments.get(3); - cursor = getThread(accountUuid, dbProjection, threadId, sortOrder); - } else { - throw new RuntimeException("Not implemented"); - } - - cursor.setNotificationUri(contentResolver, getNotificationUri(accountUuid)); - - cursor = new SpecialColumnsCursor(new IdTrickeryCursor(cursor), projection, specialColumns); - cursor = new EmailProviderCacheCursor(accountUuid, cursor); - break; - } - } - - return cursor; - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - throw new RuntimeException("not implemented yet"); - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - throw new RuntimeException("not implemented yet"); - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - throw new RuntimeException("not implemented yet"); - } - - protected Cursor getMessages(String accountUuid, final String[] projection, final String selection, - final String[] selectionArgs, final String sortOrder) { - - Account account = getAccount(accountUuid); - LockableDatabase database = getDatabase(account); - - try { - return database.execute(false, new DbCallback() { - @Override - public Cursor doDbWork(SQLiteDatabase db) { - - String where; - if (TextUtils.isEmpty(selection)) { - where = InternalMessageColumns.DELETED + " = 0 AND " + InternalMessageColumns.EMPTY + " = 0"; - } else { - where = "(" + selection + ") AND " + - InternalMessageColumns.DELETED + " = 0 AND " + InternalMessageColumns.EMPTY + " = 0"; - } - - final Cursor cursor; - if (Utility.arrayContainsAny(projection, (Object[]) FOLDERS_COLUMNS)) { - StringBuilder query = new StringBuilder(); - query.append("SELECT "); - boolean first = true; - for (String columnName : projection) { - if (!first) { - query.append(","); - } else { - first = false; - } - - if (MessageColumns.ID.equals(columnName)) { - query.append("m."); - query.append(MessageColumns.ID); - query.append(" AS "); - query.append(MessageColumns.ID); - } else { - query.append(columnName); - } - } - - query.append(" FROM messages m " + - "JOIN threads t ON (t.message_id = m.id) " + - "LEFT JOIN folders f ON (m.folder_id = f.id) " + - "WHERE "); - query.append(SqlQueryBuilder.addPrefixToSelection(FIXUP_MESSAGES_COLUMNS, "m.", where)); - query.append(" ORDER BY "); - query.append(SqlQueryBuilder.addPrefixToSelection(FIXUP_MESSAGES_COLUMNS, "m.", sortOrder)); - - cursor = db.rawQuery(query.toString(), selectionArgs); - } else { - cursor = db.query(MESSAGES_TABLE, projection, where, selectionArgs, null, null, sortOrder); - } - - return cursor; - } - }); - } catch (MessagingException e) { - throw new RuntimeException("messaging exception", e); - } - } - - protected Cursor getThreadedMessages(String accountUuid, final String[] projection, final String selection, - final String[] selectionArgs, final String sortOrder) { - - Account account = getAccount(accountUuid); - LockableDatabase database = getDatabase(account); - - try { - return database.execute(false, new DbCallback() { - @Override - public Cursor doDbWork(SQLiteDatabase db) { - - StringBuilder query = new StringBuilder(); - - query.append("SELECT "); - boolean first = true; - for (String columnName : projection) { - if (!first) { - query.append(","); - } else { - first = false; - } - - final String aggregationFunc = THREAD_AGGREGATION_FUNCS.get(columnName); - - if (MessageColumns.ID.equals(columnName)) { - query.append("m." + MessageColumns.ID + " AS " + MessageColumns.ID); - } else if (aggregationFunc != null) { - query.append("a."); - query.append(columnName); - query.append(" AS "); - query.append(columnName); - } else { - query.append(columnName); - } - } - - query.append(" FROM ("); - - createThreadedSubQuery(projection, selection, query); - - query.append(") a "); - - query.append("JOIN " + THREADS_TABLE + " t " + - "ON (t." + ThreadColumns.ROOT + " = a.thread_root) " + - "JOIN " + MESSAGES_TABLE + " m " + - "ON (m." + MessageColumns.ID + " = t." + ThreadColumns.MESSAGE_ID + " AND " + - "m." + InternalMessageColumns.EMPTY + "=0 AND " + - "m." + InternalMessageColumns.DELETED + "=0 AND " + - "m." + MessageColumns.DATE + " = a." + MessageColumns.DATE + - ") "); - - if (Utility.arrayContainsAny(projection, (Object[]) FOLDERS_COLUMNS)) { - query.append("JOIN " + FOLDERS_TABLE + " f " + - "ON (m." + MessageColumns.FOLDER_ID + " = f." + FolderColumns.ID + ") "); - } - - query.append(" GROUP BY " + ThreadColumns.ROOT); - - if (!TextUtils.isEmpty(sortOrder)) { - query.append(" ORDER BY "); - query.append(SqlQueryBuilder.addPrefixToSelection( - FIXUP_AGGREGATED_MESSAGES_COLUMNS, "a.", sortOrder)); - } - - return db.rawQuery(query.toString(), selectionArgs); - } - }); - } catch (MessagingException e) { - throw new RuntimeException("messaging exception", e); - } - } - - private void createThreadedSubQuery(String[] projection, String selection, StringBuilder query) { - query.append("SELECT t." + ThreadColumns.ROOT + " AS thread_root"); - for (String columnName : projection) { - String aggregationFunc = THREAD_AGGREGATION_FUNCS.get(columnName); - - if (SpecialColumns.THREAD_COUNT.equals(columnName)) { - query.append(",COUNT(t." + ThreadColumns.ROOT + ") AS " + SpecialColumns.THREAD_COUNT); - } else if (aggregationFunc != null) { - query.append(","); - query.append(aggregationFunc); - query.append("("); - query.append(columnName); - query.append(") AS "); - query.append(columnName); - } else { - // Skip - } - } - - query.append( - " FROM " + MESSAGES_TABLE + " m " + - "JOIN " + THREADS_TABLE + " t " + - "ON (t." + ThreadColumns.MESSAGE_ID + " = m." + MessageColumns.ID + ")"); - - if (Utility.arrayContainsAny(projection, (Object[]) FOLDERS_COLUMNS)) { - query.append(" JOIN " + FOLDERS_TABLE + " f " + - "ON (m." + MessageColumns.FOLDER_ID + " = f." + FolderColumns.ID + ")"); - } - - query.append(" WHERE (t." + ThreadColumns.ROOT + " IN (" + - "SELECT " + ThreadColumns.ROOT + " " + - "FROM " + MESSAGES_TABLE + " m " + - "JOIN " + THREADS_TABLE + " t " + - "ON (t." + ThreadColumns.MESSAGE_ID + " = m." + MessageColumns.ID + ") " + - "WHERE " + - "m." + InternalMessageColumns.EMPTY + " = 0 AND " + - "m." + InternalMessageColumns.DELETED + " = 0)"); - - - if (!TextUtils.isEmpty(selection)) { - query.append(" AND ("); - query.append(selection); - query.append(")"); - } - - query.append( - ") AND " + - InternalMessageColumns.DELETED + " = 0 AND " + InternalMessageColumns.EMPTY + " = 0"); - - query.append(" GROUP BY t." + ThreadColumns.ROOT); - } - - protected Cursor getThread(String accountUuid, final String[] projection, final String threadId, - final String sortOrder) { - - Account account = getAccount(accountUuid); - LockableDatabase database = getDatabase(account); - - try { - return database.execute(false, new DbCallback() { - @Override - public Cursor doDbWork(SQLiteDatabase db) { - - StringBuilder query = new StringBuilder(); - query.append("SELECT "); - boolean first = true; - for (String columnName : projection) { - if (!first) { - query.append(","); - } else { - first = false; - } - - if (MessageColumns.ID.equals(columnName)) { - query.append("m." + MessageColumns.ID + " AS " + MessageColumns.ID); - } else { - query.append(columnName); - } - } - - query.append(" FROM " + THREADS_TABLE + " t JOIN " + MESSAGES_TABLE + " m " + - "ON (m." + MessageColumns.ID + " = t." + ThreadColumns.MESSAGE_ID + ") "); - - if (Utility.arrayContainsAny(projection, (Object[]) FOLDERS_COLUMNS)) { - query.append("LEFT JOIN " + FOLDERS_TABLE + " f " + - "ON (m." + MessageColumns.FOLDER_ID + " = f." + FolderColumns.ID + ") "); - } - - query.append("WHERE " + - ThreadColumns.ROOT + " = ? AND " + - InternalMessageColumns.DELETED + " = 0 AND " + InternalMessageColumns.EMPTY + " = 0"); - - query.append(" ORDER BY "); - query.append(SqlQueryBuilder.addPrefixToSelection(FIXUP_MESSAGES_COLUMNS, "m.", sortOrder)); - - return db.rawQuery(query.toString(), new String[] { threadId }); - } - }); - } catch (MessagingException e) { - throw new RuntimeException("messaging exception", e); - } - } - - private Account getAccount(String accountUuid) { - if (mPreferences == null) { - mPreferences = Preferences.getPreferences(); - } - - Account account = mPreferences.getAccount(accountUuid); - - if (account == null) { - throw new IllegalArgumentException("Unknown account: " + accountUuid); - } - - return account; - } - - private LockableDatabase getDatabase(Account account) { - LocalStore localStore; - try { - localStore = DI.get(LocalStoreProvider.class).getInstance(account); - } catch (MessagingException e) { - throw new RuntimeException("Couldn't get LocalStore", e); - } - - return localStore.getDatabase(); - } - - /** - * This class is needed to make {@link androidx.cursoradapter.widget.CursorAdapter} work with our database schema. - * - *

- * {@code CursorAdapter} requires a column named {@code "_id"} containing a stable id. We use - * the column name {@code "id"} as primary key in all our tables. So this {@link CursorWrapper} - * maps all queries for {@code "_id"} to {@code "id"}. - *

- * Please note that this only works for the returned {@code Cursor}. When querying the content - * provider you still need to use {@link MessageColumns#ID}. - *

- */ - static class IdTrickeryCursor extends CursorWrapper { - public IdTrickeryCursor(Cursor cursor) { - super(cursor); - } - - @Override - public int getColumnIndex(String columnName) { - if ("_id".equals(columnName)) { - return super.getColumnIndex("id"); - } - - return super.getColumnIndex(columnName); - } - - @Override - public int getColumnIndexOrThrow(String columnName) { - if ("_id".equals(columnName)) { - return super.getColumnIndexOrThrow("id"); - } - - return super.getColumnIndexOrThrow(columnName); - } - } - - static class SpecialColumnsCursor extends CursorWrapper { - private int[] mColumnMapping; - private String[] mSpecialColumnValues; - private String[] mColumnNames; - - public SpecialColumnsCursor(Cursor cursor, String[] allColumnNames, Map specialColumns) { - super(cursor); - - mColumnNames = allColumnNames; - mColumnMapping = new int[allColumnNames.length]; - mSpecialColumnValues = new String[specialColumns.size()]; - for (int i = 0, columnIndex = 0, specialColumnCount = 0, len = allColumnNames.length; i < len; i++) { - String columnName = allColumnNames[i]; - - if (specialColumns.containsKey(columnName)) { - // This is a special column name, so save the value in mSpecialColumnValues - mSpecialColumnValues[specialColumnCount] = specialColumns.get(columnName); - - // Write the index into mSpecialColumnValues negated into mColumnMapping - mColumnMapping[i] = -(specialColumnCount + 1); - specialColumnCount++; - } else { - mColumnMapping[i] = columnIndex++; - } - } - } - - @Override - public byte[] getBlob(int columnIndex) { - int realColumnIndex = mColumnMapping[columnIndex]; - if (realColumnIndex < 0) { - throw new RuntimeException("Special column can only be retrieved as string."); - } - - return super.getBlob(realColumnIndex); - } - - @Override - public int getColumnCount() { - return mColumnMapping.length; - } - - @Override - public int getColumnIndex(String columnName) { - for (int i = 0, len = mColumnNames.length; i < len; i++) { - if (mColumnNames[i].equals(columnName)) { - return i; - } - } - - return super.getColumnIndex(columnName); - } - - @Override - public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { - int index = getColumnIndex(columnName); - if (index == -1) { - throw new IllegalArgumentException("Unknown column name"); - } - - return index; - } - - @Override - public String getColumnName(int columnIndex) { - return mColumnNames[columnIndex]; - } - - @Override - public String[] getColumnNames() { - return mColumnNames.clone(); - } - - @Override - public double getDouble(int columnIndex) { - int realColumnIndex = mColumnMapping[columnIndex]; - if (realColumnIndex < 0) { - throw new RuntimeException("Special column can only be retrieved as string."); - } - - return super.getDouble(realColumnIndex); - } - - @Override - public float getFloat(int columnIndex) { - int realColumnIndex = mColumnMapping[columnIndex]; - if (realColumnIndex < 0) { - throw new RuntimeException("Special column can only be retrieved as string."); - } - - return super.getFloat(realColumnIndex); - } - - @Override - public int getInt(int columnIndex) { - int realColumnIndex = mColumnMapping[columnIndex]; - if (realColumnIndex < 0) { - throw new RuntimeException("Special column can only be retrieved as string."); - } - - return super.getInt(realColumnIndex); - } - - @Override - public long getLong(int columnIndex) { - int realColumnIndex = mColumnMapping[columnIndex]; - if (realColumnIndex < 0) { - throw new RuntimeException("Special column can only be retrieved as string."); - } - - return super.getLong(realColumnIndex); - } - - @Override - public short getShort(int columnIndex) { - int realColumnIndex = mColumnMapping[columnIndex]; - if (realColumnIndex < 0) { - throw new RuntimeException("Special column can only be retrieved as string."); - } - - return super.getShort(realColumnIndex); - } - - @Override - public String getString(int columnIndex) { - int realColumnIndex = mColumnMapping[columnIndex]; - if (realColumnIndex < 0) { - return mSpecialColumnValues[-realColumnIndex - 1]; - } - - return super.getString(realColumnIndex); - } - - @Override - public int getType(int columnIndex) { - int realColumnIndex = mColumnMapping[columnIndex]; - if (realColumnIndex < 0) { - return FIELD_TYPE_STRING; - } - - return super.getType(realColumnIndex); - } - - @Override - public boolean isNull(int columnIndex) { - int realColumnIndex = mColumnMapping[columnIndex]; - if (realColumnIndex < 0) { - return (mSpecialColumnValues[-realColumnIndex - 1] == null); - } - - return super.isNull(realColumnIndex); - } - } -} diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendFolderTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendFolderTest.kt index 49bc6fc81d..ba1488fb01 100644 --- a/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendFolderTest.kt +++ b/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendFolderTest.kt @@ -1,7 +1,6 @@ package com.fsck.k9.mailstore import android.database.sqlite.SQLiteDatabase -import android.net.Uri import androidx.core.content.contentValuesOf import com.fsck.k9.Account import com.fsck.k9.K9RobolectricTest @@ -17,12 +16,10 @@ import com.fsck.k9.mail.MessageDownloadState import com.fsck.k9.mail.internet.MimeMessage import com.fsck.k9.mail.internet.MimeMessageHelper import com.fsck.k9.mail.internet.TextBody -import com.fsck.k9.provider.EmailProvider import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Assert.fail -import org.junit.Before import org.junit.Test import org.koin.core.component.inject @@ -36,12 +33,6 @@ class K9BackendFolderTest : K9RobolectricTest() { val backendFolder = createBackendFolder() val database: LockableDatabase = localStoreProvider.getInstance(account).database - @Before - fun setUp() { - // Set EmailProvider.CONTENT_URI so LocalStore.notifyChange() won't crash - EmailProvider.CONTENT_URI = Uri.parse("content://dummy") - } - @After fun tearDown() { preferences.deleteAccount(account) diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendStorageTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendStorageTest.kt index 780ceb9cf8..ed0009a78e 100644 --- a/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendStorageTest.kt +++ b/app/core/src/test/java/com/fsck/k9/mailstore/K9BackendStorageTest.kt @@ -1,15 +1,12 @@ package com.fsck.k9.mailstore -import android.net.Uri import com.fsck.k9.Account import com.fsck.k9.K9RobolectricTest import com.fsck.k9.Preferences import com.fsck.k9.backend.api.BackendStorage import com.fsck.k9.mail.FolderClass -import com.fsck.k9.provider.EmailProvider import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Before import org.junit.Test import org.koin.core.component.inject import org.mockito.kotlin.any @@ -26,12 +23,6 @@ class K9BackendStorageTest : K9RobolectricTest() { val database: LockableDatabase = localStoreProvider.getInstance(account).database val backendStorage = createBackendStorage() - @Before - fun setUp() { - // Set EmailProvider.CONTENT_URI so LocalStore.notifyChange() won't crash - EmailProvider.CONTENT_URI = Uri.parse("content://dummy") - } - @After fun tearDown() { preferences.deleteAccount(account) diff --git a/app/k9mail/src/main/AndroidManifest.xml b/app/k9mail/src/main/AndroidManifest.xml index 3460da1c0d..c2f32660fd 100644 --- a/app/k9mail/src/main/AndroidManifest.xml +++ b/app/k9mail/src/main/AndroidManifest.xml @@ -401,11 +401,6 @@ android:authorities="${applicationId}.messageprovider" android:exported="false" /> - - - */ - public static class ReverseComparator implements Comparator { - private Comparator mDelegate; - - /** - * @param delegate - * Never {@code null}. - */ - public ReverseComparator(final Comparator delegate) { - mDelegate = delegate; - } - - @Override - public int compare(final T object1, final T object2) { - // arg1 & 2 are mixed up, this is done on purpose - return mDelegate.compare(object2, object1); - } - } - - /** - * Chains comparator to find a non-0 result. - * - * @param - */ - public static class ComparatorChain implements Comparator { - private List> mChain; - - /** - * @param chain - * Comparator chain. Never {@code null}. - */ - public ComparatorChain(final List> chain) { - mChain = chain; - } - - @Override - public int compare(T object1, T object2) { - int result = 0; - for (final Comparator comparator : mChain) { - result = comparator.compare(object1, object2); - if (result != 0) { - break; - } - } - return result; - } - } - - public static class ReverseIdComparator implements Comparator { - private int mIdColumn = -1; - - @Override - public int compare(Cursor cursor1, Cursor cursor2) { - if (mIdColumn == -1) { - mIdColumn = cursor1.getColumnIndex("_id"); - } - long o1Id = cursor1.getLong(mIdColumn); - long o2Id = cursor2.getLong(mIdColumn); - return (o1Id > o2Id) ? -1 : 1; - } - } - - public static class AttachmentComparator implements Comparator { - - @Override - public int compare(Cursor cursor1, Cursor cursor2) { - int o1HasAttachment = (cursor1.getInt(MLFProjectionInfo.ATTACHMENT_COUNT_COLUMN) > 0) ? 0 : 1; - int o2HasAttachment = (cursor2.getInt(MLFProjectionInfo.ATTACHMENT_COUNT_COLUMN) > 0) ? 0 : 1; - return o1HasAttachment - o2HasAttachment; - } - } - - public static class FlaggedComparator implements Comparator { - - @Override - public int compare(Cursor cursor1, Cursor cursor2) { - int o1IsFlagged = (cursor1.getInt(MLFProjectionInfo.FLAGGED_COLUMN) == 1) ? 0 : 1; - int o2IsFlagged = (cursor2.getInt(MLFProjectionInfo.FLAGGED_COLUMN) == 1) ? 0 : 1; - return o1IsFlagged - o2IsFlagged; - } - } - - public static class UnreadComparator implements Comparator { - - @Override - public int compare(Cursor cursor1, Cursor cursor2) { - int o1IsUnread = cursor1.getInt(MLFProjectionInfo.READ_COLUMN); - int o2IsUnread = cursor2.getInt(MLFProjectionInfo.READ_COLUMN); - return o1IsUnread - o2IsUnread; - } - } - - public static class DateComparator implements Comparator { - - @Override - public int compare(Cursor cursor1, Cursor cursor2) { - long o1Date = cursor1.getLong(MLFProjectionInfo.DATE_COLUMN); - long o2Date = cursor2.getLong(MLFProjectionInfo.DATE_COLUMN); - return Long.compare(o1Date, o2Date); - } - } - - public static class ArrivalComparator implements Comparator { - - @Override - public int compare(Cursor cursor1, Cursor cursor2) { - long o1Date = cursor1.getLong(MLFProjectionInfo.INTERNAL_DATE_COLUMN); - long o2Date = cursor2.getLong(MLFProjectionInfo.INTERNAL_DATE_COLUMN); - return Long.compare(o1Date, o2Date); - } - } - - public static class SubjectComparator implements Comparator { - - @Override - public int compare(Cursor cursor1, Cursor cursor2) { - String subject1 = cursor1.getString(MLFProjectionInfo.SUBJECT_COLUMN); - String subject2 = cursor2.getString(MLFProjectionInfo.SUBJECT_COLUMN); - - if (subject1 == null) { - return (subject2 == null) ? 0 : -1; - } else if (subject2 == null) { - return 1; - } - - return subject1.compareToIgnoreCase(subject2); - } - } - - public static class SenderComparator implements Comparator { - - @Override - public int compare(Cursor cursor1, Cursor cursor2) { - String sender1 = MlfUtils.getSenderAddressFromCursor(cursor1); - String sender2 = MlfUtils.getSenderAddressFromCursor(cursor2); - - if (sender1 == null && sender2 == null) { - return 0; - } else if (sender1 == null) { - return 1; - } else if (sender2 == null) { - return -1; - } else { - return sender1.compareToIgnoreCase(sender2); - } - } - } -} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MlfUtils.java b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MlfUtils.java index af429d8774..eb35a43e3b 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MlfUtils.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MlfUtils.java @@ -3,7 +3,6 @@ package com.fsck.k9.fragment; import java.util.List; -import android.database.Cursor; import android.text.TextUtils; import com.fsck.k9.Account; @@ -11,14 +10,11 @@ import com.fsck.k9.DI; import com.fsck.k9.Preferences; import com.fsck.k9.controller.MessageReference; import com.fsck.k9.helper.Utility; -import com.fsck.k9.mail.Address; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mailstore.LocalFolder; import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.mailstore.LocalStoreProvider; -import static com.fsck.k9.fragment.MLFProjectionInfo.SENDER_LIST_COLUMN; - public class MlfUtils { @@ -35,12 +31,6 @@ public class MlfUtils { account.setLastSelectedFolderId(folderId); } - static String getSenderAddressFromCursor(Cursor cursor) { - String fromList = cursor.getString(SENDER_LIST_COLUMN); - Address[] fromAddrs = Address.unpack(fromList); - return (fromAddrs.length > 0) ? fromAddrs[0].getAddress() : null; - } - static String buildSubject(String subjectFromCursor, String emptySubject, int threadCount) { if (TextUtils.isEmpty(subjectFromCursor)) { return emptySubject; -- GitLab From cfa01261ae84cedc4ad129469b4f4cf830a565be Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 17:09:56 +0200 Subject: [PATCH 52/85] Rename `EmailProviderCache` to `MessageListCache` --- .../fsck/k9/controller/MessagingController.java | 16 ++++++++-------- .../k9/mailstore/CacheAwareMessageMapper.kt | 6 ++---- .../MessageListCache.kt} | 12 +++++------- .../fsck/k9/mailstore/MessageListRepository.kt | 13 ++++++------- .../MessageListCacheTest.kt} | 17 +++++++---------- .../k9/mailstore/MessageListRepositoryTest.kt | 9 ++++----- 6 files changed, 32 insertions(+), 41 deletions(-) rename app/core/src/main/java/com/fsck/k9/{cache/EmailProviderCache.kt => mailstore/MessageListCache.kt} (90%) rename app/core/src/test/java/com/fsck/k9/{cache/EmailProviderCacheTest.kt => mailstore/MessageListCacheTest.kt} (87%) diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java index 69b715d6ce..c23ab373ff 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -39,7 +39,6 @@ import com.fsck.k9.backend.api.Backend; import com.fsck.k9.backend.api.BuildConfig; import com.fsck.k9.backend.api.SyncConfig; import com.fsck.k9.backend.api.SyncListener; -import com.fsck.k9.cache.EmailProviderCache; import com.fsck.k9.controller.ControllerExtension.ControllerInternals; import com.fsck.k9.controller.MessagingControllerCommands.PendingAppend; import com.fsck.k9.controller.MessagingControllerCommands.PendingCommand; @@ -71,6 +70,7 @@ import com.fsck.k9.mailstore.LocalFolder; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.mailstore.LocalStoreProvider; +import com.fsck.k9.mailstore.MessageListCache; import com.fsck.k9.mailstore.MessageStore; import com.fsck.k9.mailstore.MessageStoreManager; import com.fsck.k9.mailstore.OutboxState; @@ -322,12 +322,12 @@ public class MessagingController { private void suppressMessages(Account account, List messages) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); + MessageListCache cache = MessageListCache.getCache(account.getUuid()); cache.hideMessages(messages); } private void unsuppressMessages(Account account, List messages) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); + MessageListCache cache = MessageListCache.getCache(account.getUuid()); cache.unhideMessages(messages); } @@ -335,14 +335,14 @@ public class MessagingController { long messageId = message.getDatabaseId(); long folderId = message.getFolder().getDatabaseId(); - EmailProviderCache cache = EmailProviderCache.getCache(message.getFolder().getAccountUuid()); + MessageListCache cache = MessageListCache.getCache(message.getFolder().getAccountUuid()); return cache.isMessageHidden(messageId, folderId); } private void setFlagInCache(final Account account, final List messageIds, final Flag flag, final boolean newState) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); + MessageListCache cache = MessageListCache.getCache(account.getUuid()); String columnName = LocalStore.getColumnNameForFlag(flag); String value = Integer.toString((newState) ? 1 : 0); cache.setValueForMessages(messageIds, columnName, value); @@ -351,7 +351,7 @@ public class MessagingController { private void removeFlagFromCache(final Account account, final List messageIds, final Flag flag) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); + MessageListCache cache = MessageListCache.getCache(account.getUuid()); String columnName = LocalStore.getColumnNameForFlag(flag); cache.removeValueForMessages(messageIds, columnName); } @@ -359,7 +359,7 @@ public class MessagingController { private void setFlagForThreadsInCache(final Account account, final List threadRootIds, final Flag flag, final boolean newState) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); + MessageListCache cache = MessageListCache.getCache(account.getUuid()); String columnName = LocalStore.getColumnNameForFlag(flag); String value = Integer.toString((newState) ? 1 : 0); cache.setValueForThreads(threadRootIds, columnName, value); @@ -368,7 +368,7 @@ public class MessagingController { private void removeFlagForThreadsFromCache(final Account account, final List messageIds, final Flag flag) { - EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid()); + MessageListCache cache = MessageListCache.getCache(account.getUuid()); String columnName = LocalStore.getColumnNameForFlag(flag); cache.removeValueForThreads(messageIds, columnName); } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt b/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt index 956b1ce563..2620bab61d 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt @@ -1,9 +1,7 @@ package com.fsck.k9.mailstore -import com.fsck.k9.cache.EmailProviderCache - internal class CacheAwareMessageMapper( - private val cache: EmailProviderCache, + private val cache: MessageListCache, private val messageMapper: MessageMapper ) : MessageMapper { override fun map(message: MessageDetailsAccessor): T? { @@ -20,7 +18,7 @@ internal class CacheAwareMessageMapper( } private class CacheAwareMessageDetailsAccessor( - private val cache: EmailProviderCache, + private val cache: MessageListCache, private val message: MessageDetailsAccessor ) : MessageDetailsAccessor by message { override val isRead: Boolean diff --git a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt similarity index 90% rename from app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.kt rename to app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt index a94584e2f2..53ffaa8b6e 100644 --- a/app/core/src/main/java/com/fsck/k9/cache/EmailProviderCache.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt @@ -1,8 +1,6 @@ -package com.fsck.k9.cache +package com.fsck.k9.mailstore import com.fsck.k9.DI -import com.fsck.k9.mailstore.LocalMessage -import com.fsck.k9.mailstore.MessageListRepository import kotlin.collections.set typealias MessageId = Long @@ -15,7 +13,7 @@ typealias AccountUuid = String /** * Cache to bridge the time needed to write (user-initiated) changes to the database. */ -class EmailProviderCache private constructor(private val accountUuid: String) { +class MessageListCache private constructor(private val accountUuid: String) { private val messageCache = mutableMapOf>() private val threadCache = mutableMapOf>() private val hiddenMessageCache = mutableMapOf() @@ -122,12 +120,12 @@ class EmailProviderCache private constructor(private val accountUuid: String) { } companion object { - private val instances = mutableMapOf() + private val instances = mutableMapOf() @JvmStatic @Synchronized - fun getCache(accountUuid: String): EmailProviderCache { - return instances.getOrPut(accountUuid) { EmailProviderCache(accountUuid) } + fun getCache(accountUuid: String): MessageListCache { + return instances.getOrPut(accountUuid) { MessageListCache(accountUuid) } } } } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt index 82abe2fc9a..cfbe04f75e 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt @@ -1,6 +1,5 @@ package com.fsck.k9.mailstore -import com.fsck.k9.cache.EmailProviderCache import java.util.concurrent.CopyOnWriteArraySet class MessageListRepository( @@ -26,7 +25,7 @@ class MessageListRepository( } /** - * Retrieve list of messages from [MessageStore] but override values with data from [EmailProviderCache]. + * Retrieve list of messages from [MessageStore] but override values with data from [MessageListCache]. */ fun getMessages( accountUuid: String, @@ -36,14 +35,14 @@ class MessageListRepository( messageMapper: MessageMapper ): List { val messageStore = messageStoreManager.getMessageStore(accountUuid) - val cache = EmailProviderCache.getCache(accountUuid) + val cache = MessageListCache.getCache(accountUuid) val mapper = CacheAwareMessageMapper(cache, messageMapper) return messageStore.getMessages(selection, selectionArgs, sortOrder, mapper) } /** - * Retrieve threaded list of messages from [MessageStore] but override values with data from [EmailProviderCache]. + * Retrieve threaded list of messages from [MessageStore] but override values with data from [MessageListCache]. */ fun getThreadedMessages( accountUuid: String, @@ -53,14 +52,14 @@ class MessageListRepository( messageMapper: MessageMapper ): List { val messageStore = messageStoreManager.getMessageStore(accountUuid) - val cache = EmailProviderCache.getCache(accountUuid) + val cache = MessageListCache.getCache(accountUuid) val mapper = CacheAwareMessageMapper(cache, messageMapper) return messageStore.getThreadedMessages(selection, selectionArgs, sortOrder, mapper) } /** - * Retrieve list of messages in a thread from [MessageStore] but override values with data from [EmailProviderCache]. + * Retrieve list of messages in a thread from [MessageStore] but override values with data from [MessageListCache]. */ fun getThread( accountUuid: String, @@ -69,7 +68,7 @@ class MessageListRepository( messageMapper: MessageMapper ): List { val messageStore = messageStoreManager.getMessageStore(accountUuid) - val cache = EmailProviderCache.getCache(accountUuid) + val cache = MessageListCache.getCache(accountUuid) val mapper = CacheAwareMessageMapper(cache, messageMapper) return messageStore.getThread(threadId, sortOrder, mapper) diff --git a/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListCacheTest.kt similarity index 87% rename from app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.kt rename to app/core/src/test/java/com/fsck/k9/mailstore/MessageListCacheTest.kt index d17f9bddc7..538b1db5a6 100644 --- a/app/core/src/test/java/com/fsck/k9/cache/EmailProviderCacheTest.kt +++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListCacheTest.kt @@ -1,8 +1,5 @@ -package com.fsck.k9.cache +package com.fsck.k9.mailstore -import com.fsck.k9.mailstore.LocalFolder -import com.fsck.k9.mailstore.LocalMessage -import com.fsck.k9.mailstore.MessageListRepository import com.google.common.truth.Truth.assertThat import java.util.UUID import org.junit.After @@ -17,7 +14,7 @@ import org.mockito.kotlin.mock private const val MESSAGE_ID = 1L private const val FOLDER_ID = 2L -class EmailProviderCacheTest { +class MessageListCacheTest { private val localFolder = mock { on { databaseId } doReturn FOLDER_ID } @@ -27,7 +24,7 @@ class EmailProviderCacheTest { on { folder } doReturn localFolder } - private val cache = EmailProviderCache.getCache(UUID.randomUUID().toString()) + private val cache = MessageListCache.getCache(UUID.randomUUID().toString()) @Before fun setUp() { @@ -47,18 +44,18 @@ class EmailProviderCacheTest { @Test fun `getCache() returns different cache for each UUID`() { - val cache = EmailProviderCache.getCache("u001") + val cache = MessageListCache.getCache("u001") - val cache2 = EmailProviderCache.getCache("u002") + val cache2 = MessageListCache.getCache("u002") assertThat(cache2).isNotSameInstanceAs(cache) } @Test fun `getCache() returns same cache for the same UUID`() { - val cache = EmailProviderCache.getCache("u001") + val cache = MessageListCache.getCache("u001") - val cache2 = EmailProviderCache.getCache("u001") + val cache2 = MessageListCache.getCache("u001") assertThat(cache2).isSameInstanceAs(cache) } diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt index 0f9735381a..bb45d89317 100644 --- a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt +++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt @@ -1,6 +1,5 @@ package com.fsck.k9.mailstore -import com.fsck.k9.cache.EmailProviderCache import com.fsck.k9.mail.Address import com.fsck.k9.message.extractors.PreviewResult import com.google.common.truth.Truth.assertThat @@ -106,7 +105,7 @@ class MessageListRepositoryTest { isForwarded = true ) ) - EmailProviderCache.getCache(accountUuid).apply { + MessageListCache.getCache(accountUuid).apply { setValueForMessages(listOf(MESSAGE_ID), "read", "1") setValueForThreads(listOf(THREAD_ROOT), "flagged", "0") } @@ -179,7 +178,7 @@ class MessageListRepositoryTest { isForwarded = true ) ) - EmailProviderCache.getCache(accountUuid).apply { + MessageListCache.getCache(accountUuid).apply { setValueForMessages(listOf(MESSAGE_ID), "read", "1") setValueForThreads(listOf(THREAD_ROOT), "flagged", "0") } @@ -277,7 +276,7 @@ class MessageListRepositoryTest { isForwarded = false ) ) - EmailProviderCache.getCache(accountUuid).apply { + MessageListCache.getCache(accountUuid).apply { setValueForMessages(listOf(MESSAGE_ID), "read", "1") setValueForThreads(listOf(THREAD_ROOT), "flagged", "0") } @@ -407,7 +406,7 @@ class MessageListRepositoryTest { @Suppress("SameParameterValue") private fun hideMessage(messageId: Long, folderId: Long) { - val cache = EmailProviderCache.getCache(accountUuid) + val cache = MessageListCache.getCache(accountUuid) val localFolder = mock { on { databaseId } doReturn folderId -- GitLab From 5418f71fc598948d4c51e0a051978f2756e13a4b Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 17:40:36 +0200 Subject: [PATCH 53/85] Change API of `MessageListCache` to better match how it is used --- .../k9/controller/MessagingController.java | 14 ++----- .../k9/mailstore/CacheAwareMessageMapper.kt | 18 ++++---- .../com/fsck/k9/mailstore/MessageListCache.kt | 32 +++++++-------- .../fsck/k9/mailstore/MessageListCacheTest.kt | 41 ++++++++++--------- .../k9/mailstore/MessageListRepositoryTest.kt | 13 +++--- 5 files changed, 58 insertions(+), 60 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java index c23ab373ff..2cf0e69e4f 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -343,34 +343,28 @@ public class MessagingController { final Flag flag, final boolean newState) { MessageListCache cache = MessageListCache.getCache(account.getUuid()); - String columnName = LocalStore.getColumnNameForFlag(flag); - String value = Integer.toString((newState) ? 1 : 0); - cache.setValueForMessages(messageIds, columnName, value); + cache.setFlagForMessages(messageIds, flag, newState); } private void removeFlagFromCache(final Account account, final List messageIds, final Flag flag) { MessageListCache cache = MessageListCache.getCache(account.getUuid()); - String columnName = LocalStore.getColumnNameForFlag(flag); - cache.removeValueForMessages(messageIds, columnName); + cache.removeFlagForMessages(messageIds, flag); } private void setFlagForThreadsInCache(final Account account, final List threadRootIds, final Flag flag, final boolean newState) { MessageListCache cache = MessageListCache.getCache(account.getUuid()); - String columnName = LocalStore.getColumnNameForFlag(flag); - String value = Integer.toString((newState) ? 1 : 0); - cache.setValueForThreads(threadRootIds, columnName, value); + cache.setValueForThreads(threadRootIds, flag, newState); } private void removeFlagForThreadsFromCache(final Account account, final List messageIds, final Flag flag) { MessageListCache cache = MessageListCache.getCache(account.getUuid()); - String columnName = LocalStore.getColumnNameForFlag(flag); - cache.removeValueForThreads(messageIds, columnName); + cache.removeFlagForThreads(messageIds, flag); } public void refreshFolderList(final Account account) { diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt b/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt index 2620bab61d..775effd710 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/CacheAwareMessageMapper.kt @@ -1,5 +1,7 @@ package com.fsck.k9.mailstore +import com.fsck.k9.mail.Flag + internal class CacheAwareMessageMapper( private val cache: MessageListCache, private val messageMapper: MessageMapper @@ -23,29 +25,29 @@ private class CacheAwareMessageDetailsAccessor( ) : MessageDetailsAccessor by message { override val isRead: Boolean get() { - return cache.getValueForMessage(message.id, "read")?.let { it == "1" } - ?: cache.getValueForThread(message.threadRoot, "read")?.let { it == "1" } + return cache.getFlagForMessage(message.id, Flag.SEEN) + ?: cache.getFlagForThread(message.threadRoot, Flag.SEEN) ?: message.isRead } override val isStarred: Boolean get() { - return cache.getValueForMessage(message.id, "flagged")?.let { it == "1" } - ?: cache.getValueForThread(message.threadRoot, "flagged")?.let { it == "1" } + return cache.getFlagForMessage(message.id, Flag.FLAGGED) + ?: cache.getFlagForThread(message.threadRoot, Flag.FLAGGED) ?: message.isStarred } override val isAnswered: Boolean get() { - return cache.getValueForMessage(message.id, "answered")?.let { it == "1" } - ?: cache.getValueForThread(message.threadRoot, "answered")?.let { it == "1" } + return cache.getFlagForMessage(message.id, Flag.ANSWERED) + ?: cache.getFlagForThread(message.threadRoot, Flag.ANSWERED) ?: message.isAnswered } override val isForwarded: Boolean get() { - return cache.getValueForMessage(message.id, "forwarded")?.let { it == "1" } - ?: cache.getValueForThread(message.threadRoot, "forwarded")?.let { it == "1" } + return cache.getFlagForMessage(message.id, Flag.FORWARDED) + ?: cache.getFlagForThread(message.threadRoot, Flag.FORWARDED) ?: message.isForwarded } } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt index 53ffaa8b6e..ed4034b8e0 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt @@ -1,65 +1,65 @@ package com.fsck.k9.mailstore import com.fsck.k9.DI +import com.fsck.k9.mail.Flag import kotlin.collections.set typealias MessageId = Long typealias ThreadId = Long typealias FolderId = Long -typealias ColumnName = String -typealias ColumnValue = String +typealias FlagValue = Boolean typealias AccountUuid = String /** * Cache to bridge the time needed to write (user-initiated) changes to the database. */ class MessageListCache private constructor(private val accountUuid: String) { - private val messageCache = mutableMapOf>() - private val threadCache = mutableMapOf>() + private val messageCache = mutableMapOf>() + private val threadCache = mutableMapOf>() private val hiddenMessageCache = mutableMapOf() - fun getValueForMessage(messageId: Long, columnName: String): String? { + fun getFlagForMessage(messageId: Long, flag: Flag): Boolean? { synchronized(messageCache) { val columnMap = messageCache[messageId] - return columnMap?.get(columnName) + return columnMap?.get(flag) } } - fun getValueForThread(threadRootId: Long, columnName: String): String? { + fun getFlagForThread(threadRootId: Long, flag: Flag): Boolean? { synchronized(threadCache) { val columnMap = threadCache[threadRootId] - return columnMap?.get(columnName) + return columnMap?.get(flag) } } - fun setValueForMessages(messageIds: List, columnName: String, value: String) { + fun setFlagForMessages(messageIds: List, flag: Flag, value: Boolean) { synchronized(messageCache) { for (messageId in messageIds) { val columnMap = messageCache.getOrPut(messageId) { mutableMapOf() } - columnMap[columnName] = value + columnMap[flag] = value } } notifyChange() } - fun setValueForThreads(threadRootIds: List, columnName: String, value: String) { + fun setValueForThreads(threadRootIds: List, flag: Flag, value: Boolean) { synchronized(threadCache) { for (threadRootId in threadRootIds) { val columnMap = threadCache.getOrPut(threadRootId) { mutableMapOf() } - columnMap[columnName] = value + columnMap[flag] = value } } notifyChange() } - fun removeValueForMessages(messageIds: List, columnName: String) { + fun removeFlagForMessages(messageIds: List, flag: Flag) { synchronized(messageCache) { for (messageId in messageIds) { val columnMap = messageCache[messageId] if (columnMap != null) { - columnMap.remove(columnName) + columnMap.remove(flag) if (columnMap.isEmpty()) { messageCache.remove(messageId) } @@ -68,12 +68,12 @@ class MessageListCache private constructor(private val accountUuid: String) { } } - fun removeValueForThreads(threadRootIds: List, columnName: String) { + fun removeFlagForThreads(threadRootIds: List, flag: Flag) { synchronized(threadCache) { for (threadRootId in threadRootIds) { val columnMap = threadCache[threadRootId] if (columnMap != null) { - columnMap.remove(columnName) + columnMap.remove(flag) if (columnMap.isEmpty()) { threadCache.remove(threadRootId) } diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListCacheTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListCacheTest.kt index 538b1db5a6..b23c4ff7e6 100644 --- a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListCacheTest.kt +++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListCacheTest.kt @@ -1,5 +1,6 @@ package com.fsck.k9.mailstore +import com.fsck.k9.mail.Flag import com.google.common.truth.Truth.assertThat import java.util.UUID import org.junit.After @@ -61,53 +62,53 @@ class MessageListCacheTest { } @Test - fun `getValueForMessage() returns value set for message`() { - cache.setValueForMessages(listOf(1L), "subject", "Subject") + fun `getFlagForMessage() returns value set for message`() { + cache.setFlagForMessages(listOf(1L), Flag.SEEN, true) - val result = cache.getValueForMessage(1L, "subject") + val result = cache.getFlagForMessage(1L, Flag.SEEN) - assertThat(result).isEqualTo("Subject") + assertThat(result).isTrue() } @Test - fun `getValueForUnknownMessage() returns null`() { - val result = cache.getValueForMessage(1L, "subject") + fun `getFlagForMessage() with unknown message ID returns null`() { + val result = cache.getFlagForMessage(1L, Flag.SEEN) assertThat(result).isNull() } @Test - fun `getValueForUnknownMessage() returns null when removed`() { - cache.setValueForMessages(listOf(1L), "subject", "Subject") - cache.removeValueForMessages(listOf(1L), "subject") + fun `getFlagForMessage() returns null when removed`() { + cache.setFlagForMessages(listOf(1L), Flag.FLAGGED, false) + cache.removeFlagForMessages(listOf(1L), Flag.FLAGGED) - val result = cache.getValueForMessage(1L, "subject") + val result = cache.getFlagForMessage(1L, Flag.FLAGGED) assertThat(result).isNull() } @Test - fun `getValueForThread() returns value set for thread`() { - cache.setValueForThreads(listOf(1L), "subject", "Subject") + fun `getFlagForThread() returns value set for thread`() { + cache.setValueForThreads(listOf(1L), Flag.SEEN, false) - val result = cache.getValueForThread(1L, "subject") + val result = cache.getFlagForThread(1L, Flag.SEEN) - assertThat(result).isEqualTo("Subject") + assertThat(result).isFalse() } @Test - fun `getValueForUnknownThread() returns null`() { - val result = cache.getValueForThread(1L, "subject") + fun `getFlagForThread() with unknown message ID returns null`() { + val result = cache.getFlagForThread(1L, Flag.ANSWERED) assertThat(result).isNull() } @Test - fun `getValueForUnknownThread() returns null when removed`() { - cache.setValueForThreads(listOf(1L), "subject", "Subject") - cache.removeValueForThreads(listOf(1L), "subject") + fun `getFlagForThread() returns null when removed`() { + cache.setValueForThreads(listOf(1L), Flag.SEEN, true) + cache.removeFlagForThreads(listOf(1L), Flag.SEEN) - val result = cache.getValueForThread(1L, "subject") + val result = cache.getFlagForThread(1L, Flag.SEEN) assertThat(result).isNull() } diff --git a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt index bb45d89317..300356f22f 100644 --- a/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt +++ b/app/core/src/test/java/com/fsck/k9/mailstore/MessageListRepositoryTest.kt @@ -1,6 +1,7 @@ package com.fsck.k9.mailstore import com.fsck.k9.mail.Address +import com.fsck.k9.mail.Flag import com.fsck.k9.message.extractors.PreviewResult import com.google.common.truth.Truth.assertThat import java.util.UUID @@ -106,8 +107,8 @@ class MessageListRepositoryTest { ) ) MessageListCache.getCache(accountUuid).apply { - setValueForMessages(listOf(MESSAGE_ID), "read", "1") - setValueForThreads(listOf(THREAD_ROOT), "flagged", "0") + setFlagForMessages(listOf(MESSAGE_ID), Flag.SEEN, true) + setValueForThreads(listOf(THREAD_ROOT), Flag.FLAGGED, false) } val result = messageListRepository.getMessages(accountUuid, SELECTION, SELECTION_ARGS, SORT_ORDER) { message -> @@ -179,8 +180,8 @@ class MessageListRepositoryTest { ) ) MessageListCache.getCache(accountUuid).apply { - setValueForMessages(listOf(MESSAGE_ID), "read", "1") - setValueForThreads(listOf(THREAD_ROOT), "flagged", "0") + setFlagForMessages(listOf(MESSAGE_ID), Flag.SEEN, true) + setValueForThreads(listOf(THREAD_ROOT), Flag.FLAGGED, false) } val result = messageListRepository.getThreadedMessages( @@ -277,8 +278,8 @@ class MessageListRepositoryTest { ) ) MessageListCache.getCache(accountUuid).apply { - setValueForMessages(listOf(MESSAGE_ID), "read", "1") - setValueForThreads(listOf(THREAD_ROOT), "flagged", "0") + setFlagForMessages(listOf(MESSAGE_ID), Flag.SEEN, true) + setValueForThreads(listOf(THREAD_ROOT), Flag.FLAGGED, false) } val result = messageListRepository.getThread( -- GitLab From 389f9f71757751d5451413934b22b9bd1a55edbc Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 19:40:27 +0200 Subject: [PATCH 54/85] Rename .java to .kt --- .../fsck/k9/mail/{BoundaryGenerator.java => BoundaryGenerator.kt} | 0 .../mail/{BoundaryGeneratorTest.java => BoundaryGeneratorTest.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename mail/common/src/main/java/com/fsck/k9/mail/{BoundaryGenerator.java => BoundaryGenerator.kt} (100%) rename mail/common/src/test/java/com/fsck/k9/mail/{BoundaryGeneratorTest.java => BoundaryGeneratorTest.kt} (100%) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.java b/mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.kt similarity index 100% rename from mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.java rename to mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.kt diff --git a/mail/common/src/test/java/com/fsck/k9/mail/BoundaryGeneratorTest.java b/mail/common/src/test/java/com/fsck/k9/mail/BoundaryGeneratorTest.kt similarity index 100% rename from mail/common/src/test/java/com/fsck/k9/mail/BoundaryGeneratorTest.java rename to mail/common/src/test/java/com/fsck/k9/mail/BoundaryGeneratorTest.kt -- GitLab From 2c2285678165405d561e7683059ca5a81ec9adeb Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 19:40:28 +0200 Subject: [PATCH 55/85] Convert `BoundaryGenerator` to Kotlin --- .../com/fsck/k9/mail/BoundaryGenerator.kt | 50 +++++++---------- .../com/fsck/k9/mail/BoundaryGeneratorTest.kt | 55 ++++++++----------- 2 files changed, 44 insertions(+), 61 deletions(-) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.kt b/mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.kt index 87c4632732..a8ce9fe875 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.kt +++ b/mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.kt @@ -1,43 +1,33 @@ -package com.fsck.k9.mail; +package com.fsck.k9.mail +import java.util.Random +import org.jetbrains.annotations.VisibleForTesting -import java.util.Random; +class BoundaryGenerator @VisibleForTesting internal constructor(private val random: Random) { -import org.jetbrains.annotations.VisibleForTesting; + fun generateBoundary(): String { + return buildString(4 + BOUNDARY_CHARACTER_COUNT) { + append("----") + repeat(BOUNDARY_CHARACTER_COUNT) { + append(BASE36_MAP[random.nextInt(36)]) + } + } + } + + companion object { + private const val BOUNDARY_CHARACTER_COUNT = 30 -public class BoundaryGenerator { - private static final BoundaryGenerator INSTANCE = new BoundaryGenerator(new Random()); - - private static final int BOUNDARY_CHARACTER_COUNT = 30; - private static final char[] BASE36_MAP = { + private val BASE36_MAP = charArrayOf( '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' - }; - - - private final Random random; + ) + private val INSTANCE = BoundaryGenerator(Random()) - public static BoundaryGenerator getInstance() { - return INSTANCE; - } - - @VisibleForTesting - BoundaryGenerator(Random random) { - this.random = random; - } - - public String generateBoundary() { - StringBuilder builder = new StringBuilder(4 + BOUNDARY_CHARACTER_COUNT); - builder.append("----"); - - for (int i = 0; i < BOUNDARY_CHARACTER_COUNT; i++) { - builder.append(BASE36_MAP[random.nextInt(36)]); - } - - return builder.toString(); + @JvmStatic + fun getInstance() = INSTANCE } } diff --git a/mail/common/src/test/java/com/fsck/k9/mail/BoundaryGeneratorTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/BoundaryGeneratorTest.kt index eca4606b7e..990a410062 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/BoundaryGeneratorTest.kt +++ b/mail/common/src/test/java/com/fsck/k9/mail/BoundaryGeneratorTest.kt @@ -1,46 +1,39 @@ -package com.fsck.k9.mail; +package com.fsck.k9.mail +import com.google.common.truth.Truth.assertThat +import java.util.Random +import org.junit.Test +import org.mockito.kotlin.mock -import java.util.Random; - -import org.junit.Test; -import org.mockito.stubbing.OngoingStubbing; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - - -public class BoundaryGeneratorTest { +class BoundaryGeneratorTest { @Test - public void generateBoundary_allZeros() throws Exception { - Random random = createRandom(0); - BoundaryGenerator boundaryGenerator = new BoundaryGenerator(random); + fun `generateBoundary() with all zeros`() { + val random = createRandom(0) + val boundaryGenerator = BoundaryGenerator(random) - String result = boundaryGenerator.generateBoundary(); + val result = boundaryGenerator.generateBoundary() - assertEquals("----000000000000000000000000000000", result); + assertThat(result).isEqualTo("----000000000000000000000000000000") } @Test - public void generateBoundary() throws Exception { - Random random = createRandom(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 35); - BoundaryGenerator boundaryGenerator = new BoundaryGenerator(random); + fun generateBoundary() { + val random = createRandom( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 35 + ) + val boundaryGenerator = BoundaryGenerator(random) - String result = boundaryGenerator.generateBoundary(); + val result = boundaryGenerator.generateBoundary() - assertEquals("----0123456789ABCDEFGHIJKLMNOPQRSZ", result); + assertThat(result).isEqualTo("----0123456789ABCDEFGHIJKLMNOPQRSZ") } - private Random createRandom(int... values) { - Random random = mock(Random.class); - - OngoingStubbing ongoingStubbing = when(random.nextInt(36)); - for (int value : values) { - ongoingStubbing = ongoingStubbing.thenReturn(value); + private fun createRandom(vararg values: Int): Random { + return mock { + var ongoingStubbing = on { nextInt(36) } + for (value in values) { + ongoingStubbing = ongoingStubbing.thenReturn(value) + } } - - return random; } } -- GitLab From 58ce09c43f1fb59264ece2c1e89bbccfeefe1dbb Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 20:11:29 +0200 Subject: [PATCH 56/85] Rename .java to .kt --- .../mail/store/imap/{FolderNameCodec.java => FolderNameCodec.kt} | 0 .../imap/{FolderNameCodecTest.java => FolderNameCodecTest.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/{FolderNameCodec.java => FolderNameCodec.kt} (100%) rename mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/{FolderNameCodecTest.java => FolderNameCodecTest.kt} (100%) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/FolderNameCodec.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/FolderNameCodec.kt similarity index 100% rename from mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/FolderNameCodec.java rename to mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/FolderNameCodec.kt diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/FolderNameCodecTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/FolderNameCodecTest.kt similarity index 100% rename from mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/FolderNameCodecTest.java rename to mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/FolderNameCodecTest.kt -- GitLab From 5f4b24d5fe6d81e4ef0cd4e5641cd14c78a1f4ae Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 20:11:29 +0200 Subject: [PATCH 57/85] Convert `FolderNameCodec` to Kotlin --- .../k9/mail/store/imap/FolderNameCodec.kt | 52 ++++++---------- .../fsck/k9/mail/store/imap/RealImapStore.kt | 2 +- .../k9/mail/store/imap/FolderNameCodecTest.kt | 59 +++++-------------- .../k9/mail/store/imap/RealImapFolderTest.kt | 2 +- 4 files changed, 34 insertions(+), 81 deletions(-) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/FolderNameCodec.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/FolderNameCodec.kt index 948b036741..9619f654de 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/FolderNameCodec.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/FolderNameCodec.kt @@ -1,43 +1,27 @@ -package com.fsck.k9.mail.store.imap; +package com.fsck.k9.mail.store.imap +import com.beetstra.jutf7.CharsetProvider +import java.nio.ByteBuffer +import java.nio.charset.CodingErrorAction +import java.nio.charset.StandardCharsets -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CodingErrorAction; +internal class FolderNameCodec { + private val modifiedUtf7Charset = CharsetProvider().charsetForName("X-RFC-3501") + private val asciiCharset = StandardCharsets.US_ASCII -import com.beetstra.jutf7.CharsetProvider; + fun encode(folderName: String): String { + val byteBuffer = modifiedUtf7Charset.encode(folderName) + val bytes = ByteArray(byteBuffer.limit()) + byteBuffer.get(bytes) - -class FolderNameCodec { - private final Charset modifiedUtf7Charset; - private final Charset asciiCharset; - - - public static FolderNameCodec newInstance() { - return new FolderNameCodec(); - } - - private FolderNameCodec() { - modifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501"); - asciiCharset = Charset.forName("US-ASCII"); - } - - public String encode(String folderName) { - ByteBuffer byteBuffer = modifiedUtf7Charset.encode(folderName); - byte[] bytes = new byte[byteBuffer.limit()]; - byteBuffer.get(bytes); - - return new String(bytes, asciiCharset); + return String(bytes, asciiCharset) } - public String decode(String encodedFolderName) throws CharacterCodingException { - CharsetDecoder decoder = modifiedUtf7Charset.newDecoder().onMalformedInput(CodingErrorAction.REPORT); - ByteBuffer byteBuffer = ByteBuffer.wrap(encodedFolderName.getBytes(asciiCharset)); - CharBuffer charBuffer = decoder.decode(byteBuffer); + fun decode(encodedFolderName: String): String { + val decoder = modifiedUtf7Charset.newDecoder().onMalformedInput(CodingErrorAction.REPORT) + val byteBuffer = ByteBuffer.wrap(encodedFolderName.toByteArray(asciiCharset)) + val charBuffer = decoder.decode(byteBuffer) - return charBuffer.toString(); + return charBuffer.toString() } } diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt index 08a907e8e5..70914a6d3a 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.kt @@ -22,7 +22,7 @@ internal open class RealImapStore( private val trustedSocketFactory: TrustedSocketFactory, private val oauthTokenProvider: OAuth2TokenProvider? ) : ImapStore, ImapConnectionManager, InternalImapStore { - private val folderNameCodec: FolderNameCodec = FolderNameCodec.newInstance() + private val folderNameCodec: FolderNameCodec = FolderNameCodec() private val host: String = checkNotNull(serverSettings.host) diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/FolderNameCodecTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/FolderNameCodecTest.kt index 78c0aa976d..b1162f81e0 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/FolderNameCodecTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/FolderNameCodecTest.kt @@ -1,59 +1,28 @@ -package com.fsck.k9.mail.store.imap; +package com.fsck.k9.mail.store.imap +import com.google.common.truth.Truth.assertThat +import org.junit.Test -import java.nio.charset.CharacterCodingException; - -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - - -public class FolderNameCodecTest { - private FolderNameCodec folderNameCode; - - - @Before - public void setUp() throws Exception { - folderNameCode = FolderNameCodec.newInstance(); - } +class FolderNameCodecTest { + private var folderNameCode = FolderNameCodec() @Test - public void encode_withAsciiArgument_shouldReturnInput() throws Exception { - String folderName = "ASCII"; - - String result = folderNameCode.encode(folderName); - - assertEquals(folderName, result); + fun `encode() with ASCII argument should return input`() { + assertThat(folderNameCode.encode("ASCII")).isEqualTo("ASCII") } @Test - public void encode_withNonAsciiArgument_shouldReturnEncodedString() throws Exception { - String folderName = "über"; - - String result = folderNameCode.encode(folderName); - - assertEquals("&APw-ber", result); + fun `encode() with non-ASCII argument should return encoded string`() { + assertThat(folderNameCode.encode("über")).isEqualTo("&APw-ber") } @Test - public void decode_withEncodedArgument_shouldReturnDecodedString() throws Exception { - String encodedFolderName = "&ANw-bergr&APYA3w-entr&AOQ-ger"; - - String result = folderNameCode.decode(encodedFolderName); - - assertEquals("Übergrößenträger", result); + fun `decode() with encoded argument should return decoded string`() { + assertThat(folderNameCode.decode("&ANw-bergr&APYA3w-entr&AOQ-ger")).isEqualTo("Übergrößenträger") } - @Test - public void decode_withInvalidEncodedArgument_shouldThrow() throws Exception { - String encodedFolderName = "&12-foo"; - - try { - folderNameCode.decode(encodedFolderName); - fail("Expected exception"); - } catch (CharacterCodingException ignored) { - } + @Test(expected = CharacterCodingException::class) + fun `decode() with invalid encoded argument should throw`() { + folderNameCode.decode("&12-foo") } } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt index b7e6d7d3b0..73a7744e8f 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt @@ -1096,7 +1096,7 @@ class RealImapFolderTest { private fun extractMessageUids(messages: List) = messages.map { it.uid }.toSet() private fun createFolder(folderName: String): RealImapFolder { - return RealImapFolder(internalImapStore, testConnectionManager, folderName, FolderNameCodec.newInstance()) + return RealImapFolder(internalImapStore, testConnectionManager, folderName, FolderNameCodec()) } private fun createImapMessage(uid: String): ImapMessage { -- GitLab From a54784fb4e3701ff785f2c06fa6fc8011255392c Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 20:42:43 +0200 Subject: [PATCH 58/85] Rename .java to .kt --- .../com/fsck/k9/mail/store/imap/{IdGrouper.java => IdGrouper.kt} | 0 .../k9/mail/store/imap/{IdGrouperTest.java => IdGrouperTest.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/{IdGrouper.java => IdGrouper.kt} (100%) rename mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/{IdGrouperTest.java => IdGrouperTest.kt} (100%) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/IdGrouper.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/IdGrouper.kt similarity index 100% rename from mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/IdGrouper.java rename to mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/IdGrouper.kt diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/IdGrouperTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/IdGrouperTest.kt similarity index 100% rename from mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/IdGrouperTest.java rename to mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/IdGrouperTest.kt -- GitLab From 5800627cb872dc4726a5e4237e046eeccbeb0321 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 31 Aug 2022 20:42:43 +0200 Subject: [PATCH 59/85] Convert `IdGrouper` to Kotlin --- .../com/fsck/k9/mail/store/imap/IdGrouper.kt | 108 ++++++------------ .../mail/store/imap/ImapCommandSplitter.java | 3 - .../fsck/k9/mail/store/imap/IdGrouperTest.kt | 78 +++++-------- .../store/imap/ImapCommandSplitterTest.java | 7 +- 4 files changed, 70 insertions(+), 126 deletions(-) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/IdGrouper.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/IdGrouper.kt index 439e3b7d78..32964742f8 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/IdGrouper.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/IdGrouper.kt @@ -1,94 +1,62 @@ -package com.fsck.k9.mail.store.imap; +package com.fsck.k9.mail.store.imap +private const val NO_VALID_ID = -1L -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; +internal object IdGrouper { + fun groupIds(ids: Set): GroupedIds { + require(ids.isNotEmpty()) { "groupIds() must be called with non-empty set of IDs" } + if (ids.size < 2) return GroupedIds(ids, emptyList()) -class IdGrouper { - static GroupedIds groupIds(Set ids) { - if (ids == null || ids.isEmpty()) { - throw new IllegalArgumentException("groupId() must be called with non-empty set of ids"); - } - - if (ids.size() < 2) { - return new GroupedIds(ids, Collections.emptyList()); - } + val orderedIds = ids.toSortedSet() + val firstId = orderedIds.first() - TreeSet orderedIds = new TreeSet<>(ids); - Iterator orderedIdIterator = orderedIds.iterator(); - Long previousId = orderedIdIterator.next(); + val remainingIds = mutableSetOf(firstId) + val idGroups = mutableListOf() - TreeSet remainingIds = new TreeSet<>(); - remainingIds.add(previousId); - List idGroups = new ArrayList<>(); - long currentIdGroupStart = -1L; - long currentIdGroupEnd = -1L; - while (orderedIdIterator.hasNext()) { - Long currentId = orderedIdIterator.next(); + var previousId = firstId + var currentIdGroupStart = NO_VALID_ID + var currentIdGroupEnd = NO_VALID_ID + for (currentId in orderedIds.asSequence().drop(1)) { if (previousId + 1L == currentId) { - if (currentIdGroupStart == -1L) { - remainingIds.remove(previousId); - currentIdGroupStart = previousId; - currentIdGroupEnd = currentId; + if (currentIdGroupStart == NO_VALID_ID) { + remainingIds.remove(previousId) + currentIdGroupStart = previousId + currentIdGroupEnd = currentId } else { - currentIdGroupEnd = currentId; + currentIdGroupEnd = currentId } } else { - if (currentIdGroupStart != -1L) { - idGroups.add(new ContiguousIdGroup(currentIdGroupStart, currentIdGroupEnd)); - currentIdGroupStart = -1L; + if (currentIdGroupStart != NO_VALID_ID) { + idGroups.add(ContiguousIdGroup(currentIdGroupStart, currentIdGroupEnd)) + currentIdGroupStart = NO_VALID_ID } - remainingIds.add(currentId); + remainingIds.add(currentId) } - previousId = currentId; + previousId = currentId } - if (currentIdGroupStart != -1L) { - idGroups.add(new ContiguousIdGroup(currentIdGroupStart, currentIdGroupEnd)); + if (currentIdGroupStart != NO_VALID_ID) { + idGroups.add(ContiguousIdGroup(currentIdGroupStart, currentIdGroupEnd)) } - return new GroupedIds(remainingIds, idGroups); + return GroupedIds(remainingIds, idGroups) } +} - - static class GroupedIds { - public final Set ids; - public final List idGroups; - - - GroupedIds(Set ids, List idGroups) { - if (ids.isEmpty() && idGroups.isEmpty()) { - throw new IllegalArgumentException("Must have at least one id"); - } - - this.ids = ids; - this.idGroups = idGroups; - } +internal class GroupedIds(@JvmField val ids: Set, @JvmField val idGroups: List) { + init { + require(ids.isNotEmpty() || idGroups.isNotEmpty()) { "Must have at least one ID" } } +} - static class ContiguousIdGroup { - public final long start; - public final long end; - - - ContiguousIdGroup(long start, long end) { - if (start >= end) { - throw new IllegalArgumentException("start >= end"); - } - - this.start = start; - this.end = end; - } +internal class ContiguousIdGroup(val start: Long, val end: Long) { + init { + require(start < end) { "start >= end" } + } - @Override - public String toString() { - return start + ":" + end; - } + override fun toString(): String { + return "$start:$end" } } diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapCommandSplitter.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapCommandSplitter.java index 486b6a8384..91bbcd715b 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapCommandSplitter.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapCommandSplitter.java @@ -6,9 +6,6 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; -import com.fsck.k9.mail.store.imap.IdGrouper.ContiguousIdGroup; -import com.fsck.k9.mail.store.imap.IdGrouper.GroupedIds; - class ImapCommandSplitter { static List splitCommand(String prefix, String suffix, GroupedIds groupedIds, int lengthLimit) { diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/IdGrouperTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/IdGrouperTest.kt index 375c510aa0..28342a0c35 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/IdGrouperTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/IdGrouperTest.kt @@ -1,73 +1,53 @@ -package com.fsck.k9.mail.store.imap; +package com.fsck.k9.mail.store.imap +import com.google.common.truth.Truth.assertThat +import org.junit.Test -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - - -public class IdGrouperTest { +class IdGrouperTest { @Test - public void groupIds_withSingleContiguousGroup() throws Exception { - Set ids = newSet(1L, 2L, 3L); + fun `groupIds() with single contiguous group`() { + val ids = setOf(1L, 2L, 3L) - IdGrouper.GroupedIds groupedIds = IdGrouper.groupIds(ids); + val groupedIds = IdGrouper.groupIds(ids) - assertEquals(0, groupedIds.ids.size()); - assertEquals(1, groupedIds.idGroups.size()); - assertEquals("1:3", groupedIds.idGroups.get(0).toString()); + assertThat(groupedIds.ids).isEmpty() + assertThat(groupedIds.idGroups.mapToString()).containsExactly("1:3") } @Test - public void groupIds_withoutContiguousGroup() throws Exception { - Set ids = newSet(23L, 42L, 2L, 5L); + fun `groupIds() without contiguous group`() { + val ids = setOf(23L, 42L, 2L, 5L) - IdGrouper.GroupedIds groupedIds = IdGrouper.groupIds(ids); + val groupedIds = IdGrouper.groupIds(ids) - assertEquals(ids, groupedIds.ids); - assertEquals(0, groupedIds.idGroups.size()); + assertThat(groupedIds.ids).isEqualTo(ids) + assertThat(groupedIds.idGroups).isEmpty() } @Test - public void groupIds_withMultipleContiguousGroups() throws Exception { - Set ids = newSet(1L, 3L, 4L, 5L, 6L, 10L, 12L, 13L, 14L, 23L); + fun `groupIds() with multiple contiguous groups`() { + val ids = setOf(1L, 3L, 4L, 5L, 6L, 10L, 12L, 13L, 14L, 23L) - IdGrouper.GroupedIds groupedIds = IdGrouper.groupIds(ids); + val groupedIds = IdGrouper.groupIds(ids) - assertEquals(newSet(1L, 10L, 23L), groupedIds.ids); - assertEquals(2, groupedIds.idGroups.size()); - assertEquals("3:6", groupedIds.idGroups.get(0).toString()); - assertEquals("12:14", groupedIds.idGroups.get(1).toString()); + assertThat(groupedIds.ids).containsExactly(1L, 10L, 23L) + assertThat(groupedIds.idGroups.mapToString()).containsExactly("3:6", "12:14") } @Test - public void groupIds_withSingleId() throws Exception { - Set ids = newSet(23L); - - IdGrouper.GroupedIds groupedIds = IdGrouper.groupIds(ids); + fun `groupIds() with single ID`() { + val ids = setOf(23L) - assertEquals(newSet(23L), groupedIds.ids); - assertEquals(0, groupedIds.idGroups.size()); - } + val groupedIds = IdGrouper.groupIds(ids) - @Test(expected = IllegalArgumentException.class) - public void groupIds_withEmptySet_shouldThrow() throws Exception { - IdGrouper.groupIds(newSet()); + assertThat(groupedIds.ids).containsExactly(23L) + assertThat(groupedIds.idGroups).isEmpty() } - @Test(expected = IllegalArgumentException.class) - public void groupIds_withNullArgument_shouldThrow() throws Exception { - IdGrouper.groupIds(null); - } - - - private static Set newSet(Long... values) { - HashSet set = new HashSet<>(values.length); - set.addAll(Arrays.asList(values)); - return set; + @Test(expected = IllegalArgumentException::class) + fun `groupIds() with empty set should throw`() { + IdGrouper.groupIds(emptySet()) } } + +private fun List.mapToString() = map { it.toString() } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapCommandSplitterTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapCommandSplitterTest.java index 1c4e66f90d..a65b1c5276 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapCommandSplitterTest.java +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapCommandSplitterTest.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; -import com.fsck.k9.mail.store.imap.IdGrouper.GroupedIds; import com.google.common.collect.Sets; import org.junit.Test; @@ -24,7 +23,7 @@ public class ImapCommandSplitterTest { @Test public void splitCommand_withManyNonContiguousIds_shouldSplitCommand() throws Exception { Set ids = createNonContiguousIdSet(10000, 10500, 2); - GroupedIds groupedIds = new GroupedIds(ids, Collections.emptyList()); + GroupedIds groupedIds = new GroupedIds(ids, Collections.emptyList()); List commands = ImapCommandSplitter.splitCommand(COMMAND_PREFIX, COMMAND_SUFFIX, groupedIds, 980); @@ -39,7 +38,7 @@ public class ImapCommandSplitterTest { Set idSet = Sets.union( createNonContiguousIdSet(10000, 10298, 2), createNonContiguousIdSet(10402, 10500, 2)); - List idGroups = singletonList(new IdGrouper.ContiguousIdGroup(10300L, 10400L)); + List idGroups = singletonList(new ContiguousIdGroup(10300L, 10400L)); GroupedIds groupedIds = new GroupedIds(idSet, idGroups); List commands = ImapCommandSplitter.splitCommand(COMMAND_PREFIX, COMMAND_SUFFIX, groupedIds, 980); @@ -55,7 +54,7 @@ public class ImapCommandSplitterTest { @Test public void splitCommand_withEmptySuffix_shouldCreateCommandWithoutTrailingSpace() throws Exception { Set ids = createNonContiguousIdSet(1, 2, 1); - GroupedIds groupedIds = new GroupedIds(ids, Collections.emptyList()); + GroupedIds groupedIds = new GroupedIds(ids, Collections.emptyList()); List commands = ImapCommandSplitter.splitCommand("UID SEARCH UID", "", groupedIds, 980); -- GitLab From 034eac884785ed2a42f7f0fe8871c3bd70fb3b6c Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 1 Sep 2022 16:07:00 +0200 Subject: [PATCH 60/85] Add fast path to `MessageListRepository` to bypass an empty cache --- .../com/fsck/k9/mailstore/MessageListCache.kt | 16 ++++++++++++++++ .../fsck/k9/mailstore/MessageListRepository.kt | 6 +++--- .../java/com/fsck/k9/mailstore/MessageStore.kt | 6 +++--- .../fsck/k9/storage/messages/K9MessageStore.kt | 6 +++--- .../messages/RetrieveMessageListOperations.kt | 6 +++--- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt index ed4034b8e0..51c1a8575c 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListCache.kt @@ -114,6 +114,22 @@ class MessageListCache private constructor(private val accountUuid: String) { } } + fun isEmpty(): Boolean { + return isMessageCacheEmpty() && isThreadCacheEmpty() && isHiddenMessageCacheEmpty() + } + + private fun isMessageCacheEmpty(): Boolean { + return synchronized(messageCache) { messageCache.isEmpty() } + } + + private fun isThreadCacheEmpty(): Boolean { + return synchronized(threadCache) { threadCache.isEmpty() } + } + + private fun isHiddenMessageCacheEmpty(): Boolean { + return synchronized(hiddenMessageCache) { hiddenMessageCache.isEmpty() } + } + private fun notifyChange() { val messageListRepository = DI.get() messageListRepository.notifyMessageListChanged(accountUuid) diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt index cfbe04f75e..5a748ee272 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageListRepository.kt @@ -37,7 +37,7 @@ class MessageListRepository( val messageStore = messageStoreManager.getMessageStore(accountUuid) val cache = MessageListCache.getCache(accountUuid) - val mapper = CacheAwareMessageMapper(cache, messageMapper) + val mapper = if (cache.isEmpty()) messageMapper else CacheAwareMessageMapper(cache, messageMapper) return messageStore.getMessages(selection, selectionArgs, sortOrder, mapper) } @@ -54,7 +54,7 @@ class MessageListRepository( val messageStore = messageStoreManager.getMessageStore(accountUuid) val cache = MessageListCache.getCache(accountUuid) - val mapper = CacheAwareMessageMapper(cache, messageMapper) + val mapper = if (cache.isEmpty()) messageMapper else CacheAwareMessageMapper(cache, messageMapper) return messageStore.getThreadedMessages(selection, selectionArgs, sortOrder, mapper) } @@ -70,7 +70,7 @@ class MessageListRepository( val messageStore = messageStoreManager.getMessageStore(accountUuid) val cache = MessageListCache.getCache(accountUuid) - val mapper = CacheAwareMessageMapper(cache, messageMapper) + val mapper = if (cache.isEmpty()) messageMapper else CacheAwareMessageMapper(cache, messageMapper) return messageStore.getThread(threadId, sortOrder, mapper) } } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt index 7f85c5638c..724f78083a 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt @@ -127,7 +127,7 @@ interface MessageStore { selection: String, selectionArgs: Array, sortOrder: String, - messageMapper: MessageMapper + messageMapper: MessageMapper ): List /** @@ -137,13 +137,13 @@ interface MessageStore { selection: String, selectionArgs: Array, sortOrder: String, - messageMapper: MessageMapper + messageMapper: MessageMapper ): List /** * Retrieve list of messages in a thread. */ - fun getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper): List + fun getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper): List /** * Retrieve the date of the oldest message in the given folder. diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt index 60b4f0ee76..ee69f27d90 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt @@ -106,7 +106,7 @@ class K9MessageStore( selection: String, selectionArgs: Array, sortOrder: String, - messageMapper: MessageMapper + messageMapper: MessageMapper ): List { return retrieveMessageListOperations.getMessages(selection, selectionArgs, sortOrder, messageMapper) } @@ -115,12 +115,12 @@ class K9MessageStore( selection: String, selectionArgs: Array, sortOrder: String, - messageMapper: MessageMapper + messageMapper: MessageMapper ): List { return retrieveMessageListOperations.getThreadedMessages(selection, selectionArgs, sortOrder, messageMapper) } - override fun getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper): List { + override fun getThread(threadId: Long, sortOrder: String, messageMapper: MessageMapper): List { return retrieveMessageListOperations.getThread(threadId, sortOrder, messageMapper) } diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt index 3dff6cc596..2d01b18305 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt @@ -16,7 +16,7 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka selection: String, selectionArgs: Array, sortOrder: String, - mapper: MessageMapper + mapper: MessageMapper ): List { return lockableDatabase.execute(false) { database -> database.rawQuery( @@ -66,7 +66,7 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka selection: String, selectionArgs: Array, sortOrder: String, - mapper: MessageMapper + mapper: MessageMapper ): List { val orderBy = SqlQueryBuilder.addPrefixToSelection(AGGREGATED_MESSAGES_COLUMNS, "aggregated.", sortOrder) @@ -142,7 +142,7 @@ internal class RetrieveMessageListOperations(private val lockableDatabase: Locka } } - fun getThread(threadId: Long, sortOrder: String, mapper: MessageMapper): List { + fun getThread(threadId: Long, sortOrder: String, mapper: MessageMapper): List { return lockableDatabase.execute(false) { database -> database.rawQuery( """ -- GitLab From bdef79a6fc7334737f2507437754262fc80ced77 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 2 Sep 2022 15:17:21 +0200 Subject: [PATCH 61/85] Be more selective when setting the "active message" for the message list --- .../MessageViewContainerFragment.kt | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt index ee83695a91..990a202de2 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt @@ -32,6 +32,8 @@ class MessageViewContainerFragment : Fragment() { lateinit var messageReference: MessageReference private set + private var activeMessageReference: MessageReference? = null + var lastDirection: Direction? = null private set @@ -140,22 +142,23 @@ class MessageViewContainerFragment : Fragment() { } private fun setActiveMessage(position: Int) { - rememberNavigationDirection(position, messageReference) + // If the position of current message changes (e.g. because messages were added or removed from the list), we + // keep track of the new position but otherwise ignore the event. + val newMessageReference = adapter.getMessageReference(position) + if (newMessageReference == activeMessageReference) { + currentPosition = position + return + } + + rememberNavigationDirection(position) messageReference = adapter.getMessageReference(position) + activeMessageReference = messageReference fragmentListener.setActiveMessage(messageReference) } - private fun rememberNavigationDirection(newPosition: Int, currentMessageReference: MessageReference) { - // When messages are added or removed from the list, the current position will change even though we're still - // displaying the same message. In those cases we don't want to update `lastDirection`. - val newMessageReference = adapter.getMessageReference(newPosition) - if (newMessageReference == currentMessageReference) { - currentPosition = newPosition - return - } - + private fun rememberNavigationDirection(newPosition: Int) { currentPosition?.let { currentPosition -> lastDirection = if (newPosition < currentPosition) Direction.PREVIOUS else Direction.NEXT } -- GitLab From ab9ac67a575d4b92d2707e95bbbd0ee0508dd174 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 2 Sep 2022 15:28:00 +0200 Subject: [PATCH 62/85] Remove redundant calls to `MessageListFragment.setActiveMessage()` --- .../src/main/java/com/fsck/k9/activity/MessageList.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt index 1534f3cb26..59b24e05ab 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt @@ -352,8 +352,6 @@ open class MessageList : showMessageViewPlaceHolder() } else { messageViewContainerFragment.isActive = true - val activeMessage = messageViewContainerFragment.messageReference - messageListFragment.setActiveMessage(activeMessage) } } @@ -985,10 +983,6 @@ open class MessageList : if (draftsFolderId != null && folderId == draftsFolderId) { MessageActions.actionEditDraft(this, messageReference) } else { - if (messageListFragment != null) { - messageListFragment!!.setActiveMessage(messageReference) - } - val fragment = MessageViewContainerFragment.newInstance(messageReference) supportFragmentManager.commitNow { replace(R.id.message_view_container, fragment, FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER) -- GitLab From 88c1297868c476b2be13156e63ddd2ffd1cbe58c Mon Sep 17 00:00:00 2001 From: Tobias Preuss Date: Fri, 2 Sep 2022 17:11:56 +0200 Subject: [PATCH 63/85] RIP jcenter. --- build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index 1e6d396e76..8e5d1bee6b 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,6 @@ buildscript { mavenCentral() google() maven { url "https://plugins.gradle.org/m2/" } - jcenter() } dependencies { @@ -80,7 +79,6 @@ subprojects { repositories { mavenCentral() google() - jcenter() maven { url 'https://jitpack.io' } } -- GitLab From 83b50c6edb0b1d0ed8c1b63f4cc9b892969cb735 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 3 Sep 2022 15:14:01 +0200 Subject: [PATCH 64/85] Update translations --- .../legacy/src/main/res/values-de/strings.xml | 2 +- .../legacy/src/main/res/values-eu/strings.xml | 78 ++++++++++++++++--- .../legacy/src/main/res/values-fa/strings.xml | 18 ++++- .../legacy/src/main/res/values-hu/strings.xml | 41 ++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 10 +-- .../legacy/src/main/res/values-uk/strings.xml | 16 ++++ .../src/main/res/values-zh-rCN/strings.xml | 2 +- .../src/main/res/values-zh-rTW/strings.xml | 2 + 8 files changed, 152 insertions(+), 17 deletions(-) diff --git a/app/ui/legacy/src/main/res/values-de/strings.xml b/app/ui/legacy/src/main/res/values-de/strings.xml index 8862df3513..900c5f9a61 100644 --- a/app/ui/legacy/src/main/res/values-de/strings.xml +++ b/app/ui/legacy/src/main/res/values-de/strings.xml @@ -862,7 +862,7 @@ Bitte senden Sie Fehlerberichte, Ideen für neue Funktionen und stellen Sie Frag Nachrichten eines Diskussionsstranges zusammenfassen Datenbankaktualisierung Datenbanken werden aktualisiert… - Aktualisiere Datenbank für Konto \'%s\' + Aktualisiere Datenbank für Konto \"%s\" Geteilte Ansicht Immer Nie diff --git a/app/ui/legacy/src/main/res/values-eu/strings.xml b/app/ui/legacy/src/main/res/values-eu/strings.xml index edcd105d6e..68dbc4b69d 100644 --- a/app/ui/legacy/src/main/res/values-eu/strings.xml +++ b/app/ui/legacy/src/main/res/values-eu/strings.xml @@ -12,6 +12,8 @@ Apache lizentzia, 2.0 bertsioa Kode Irekiko Proiektua Webgunea + Push Informazioa + Lortu laguntza Erabiltzaileen foroa Fedibertsoa Twitter @@ -70,6 +72,8 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Birbidali eranskin gisa Aukeratu Kontua Aukeratu karpeta + Mugitu hona… + Kopiatu hona… %d hautatuta Hurrengoa Aurrekoa @@ -82,7 +86,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Erantzun denei Ezabatu Artxibatu - Spam + Zabor-posta Birbidali Birbidali eranskin gisa Editatu mezu berri gisa @@ -102,6 +106,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Bilatu Bilatu leku guztietan Bilaketaren emaitzak + Mezu berriak Ezarpenak Kudeatu karpetak Kontu ezarpenak @@ -112,6 +117,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Gehitu izarra Kendu izarra Kopiatu + Harpidetza kendu Erakutsi goiburuak Helbidea arbelera kopiatuta @@ -155,13 +161,16 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Ezabatu denak Artxibatu Artxibatu denak - Spam + Zabor-posta + Ziurtagiri errorea %s-(r)entzako ziurtagiri errorea Egiaztatu zerbitzariaren ezarpenak Autentifikazioak huts egin du Autentifikazioak huts egin du %s-(e)rako. Eguneratu zerbitzariaren ezarpenak. + Jakinarazpen errorea + Errore bat gertatu da mezu berri baterako sistemaren jakinarazpena sortzen saiatzean. Arrazoia ziurrenik jakinarazpen-soinu bat falta da.\n\nSakatu jakinarazpen-ezarpenak irekitzeko. Posta egiaztatzen: %1$s:%2$s Posta egiaztatzen Posta bidaltzen: %s @@ -183,7 +192,10 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Gaitu arazketarako egunkaria Diagnostikorako informazio gehigarria erregistratu Bereziki babestutako informazioa erregistratu - Saio hasieretan pasahitzak erakuts daitezke. + Log-etan pasahitzak erakuts daitezke. + Esportatu logak + Ongi esportatuta. Log-ek informazio sentikorra eduki dezakate. Kontuz nori bidaltzen diozun. + Esportazioak huts egin du Kargatu mezu gehiago Nori:%s Gaia @@ -230,6 +242,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Erabili hartzaileen izenak Kontaktuetatik erabilgarri daudenean Koloreztatu kontaktuak Koloreztatu izenak kontaktu zerrendan + Kontaktu-izenaren kolorea Zabalera finkoko letra-tipoak Erabili zabalera finkoko letra-tipoak testu arrunteko mezuak erakustean Doitu mezuak automatikoki @@ -242,7 +255,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Erakutsi elkarrizketa bat hautatutako ekintzak egitean Ezabatu Ezabatu Izardunak (mezu ikuspegian) - Spam + Zabor-posta Baztertu mezua Markatu mezu guztiak irakurritako gisa Ezabatu (jakinarazpenetatik) @@ -257,6 +270,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Jakinarazpenak Blokeatutako Pantailan Jakinarazpenik ez blokeatutako pantailan Aplikazioaren izena + Mezu berrien kopurua Mezu kopurua eta bidaltzaileak Pantaila desblokeatuta dagoenean bezala Alertarik gabeko aldia @@ -268,9 +282,15 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Konfiguratu kontu berria Posta helbidea Pasahitza + Posta elektronikoko kontu hau K-9 Mail-ekin erabiltzeko, saioa hasi eta aplikazioari zure mezu elektronikoetarako sarbidea eman behar diozu. + Saioa hasi + Saioa hasi Googlerekin + Pasahitza hemen ikusteko, gaitu pantailaren blokeoa gailu honetan. + Egiaztatu zure identitatea + Desblokeatu zure pasahitza ikusteko Eskuzko konfigurazioa Kontuaren informazioa berreskuratzen\u2026 @@ -290,6 +310,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Pasahitza, era ez seguruan transmitituta Zifratutako pasahitza Bezero ziurtagiria + OAuth 2.0 Sarrerako zerbitzariaren ezarpenak Erabiltzaile izena Pasahitza @@ -308,6 +329,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Ez ezabatu zerbitzarian Zerbitzarian ezabatu Zerbitzarian irakurritako gisa markatu + Erabili konpresioa Ezabatu hemen ere zerbitzaritik ezabatutako mezuak Berehala Atzitzean @@ -317,8 +339,8 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Zirriborroen karpeta Bidalitakoen karpeta Zakarrontziaren karpeta - Artxiboaren karpeta - Spam karpeta + Artxibatuen karpeta + Zabor-posta karpeta Erakutsi harpidetutako karpetak bakarrik Automatikoki irekitako karpeta OWA bide-izena @@ -372,6 +394,10 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Erabiltzaile izen edo pasahitz okerrak.\n(%s) Zerbitzariak SSL ziurtagiri baliogabe bat aurkeztu du. Batzuetan, hau zerbitzariak konfigurazio okerra duelako da. Beste batzuetan norbait zu edo zure posta zerbitzaria erasotzen saiatzen ari delako da. Ez bazaude ziur zer ari den gertatzen, klikatu Baztertu eta jarri harremanetan zure posta zerbitzaria kudeatzen duen jendearekin.\n\n(%s) Ezin zerbitzarira konektatu.\n(%s) + baimena bertan behera utzi da + Baimenak huts egin du errore honekin: %s + OAuth 2.0 une honetan ez dago hornitzaile honekin onartuta. + Aplikazioak ezin izan du zure konturako sarbidea erabiltzeko arakatzailerik aurkitu. Editatu xehetasunak Jarraitu Aurreratua @@ -391,10 +417,15 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Erakutsi jakinarazpen bat bidaltzen ditudan mezuetarako Kontaktuak bakarrik Erakutsi kontaktu ezagunen mezuen jakinarazpenak bakarrik + Baztertu txateko mezuak + Ez erakutsi posta elektronikoko txat bateko mezuen jakinarazpenak Markatu irakurritako gisa irekitzean Markatu mezua irakurritako gisa ikusteko irekitzerakoan Markatu irakurritako gisa ezabatzean Markatu mezua irakurritako gisa ikusteko ezabatzean + Jakinarazpen kategoriak + Konfiguratu mezu berrien jakinarazpenak + Konfiguratu akatsen eta egoeraren jakinarazpenak Erakutsi beti Irudiak Ez Kontaktuetatik @@ -477,7 +508,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko 1. eta 2. klaseko karpetak Guztiak 2. klaseko karpetak izan ezik Bat ere ez - Karpeten \'Push\'a + Karpeten \"Push\"a Guztiak 1. klaseko karpetak bakarrik 1. eta 2. klaseko karpetak @@ -507,6 +538,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Klaserik ez 1. klasea 2. klasea + Inkesta moduaren araberakoak Jakinarazpenen karpeta klasea Klaserik ez 1. klasea @@ -520,11 +552,29 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Kontu izena Zure izena Jakinarazpenak + Bibrazioa Bibratu + Bibrazio eredua Lehenetsia + 1 Eredua + 2 Eredua + 3 Eredua + 4 Eredua + 5 Eredua Errepikatu bibrazioa + Desgaitua Posta berrirako txirrin-tonua + Jakinarazpen argia + Desgaitua Kontuaren kolorea + Sistemaren kolore lehenetsia + Txuria + Gorria + Berdea + Urdina + Horia + Zian + Magenta Mezuak idazteko aukerak Idazterakoan lehenetsiak Ezarri Nork, Bcc eta sinadura lehenetsiak @@ -635,6 +685,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Mezu barruko ikuspegiak Zerrenda barruko ikuspegiak Erakutsi Sarrera Ontzi Bateratua + Erakutsi kontu izardunak Sarrerako ontzi bateratua Karpeta bateratuetako mezu guztiak Bateratu @@ -701,10 +752,10 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Zakarrontzia hustu nahi duzu? Bai Ez - Berretsi spam karpetara mugitzea + Berretsi zabor-posta karpetara mugitzea Benetan mezu hau spamera mugitu nahi duzula? - Benetan %1$d mezu spam karpetara mugitu nahi dituzula? + Benetan %1$d mezu zabor-posta karpetara mugitu nahi dituzula? Bai Ez @@ -726,13 +777,16 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Sartu pasahitzak + Mesedez saioa hasi + Mesedez saioa hasi eta sartu pasahitza Ezin izan dira ezarpenak inportatu Ezin izan da ezarpenen fitxategia irakurri Ezin izan dira ezarpen batzuk inportatu Ongi inportatuta Pasahitza derrigorrezkoa + Saio-haztea beharrezkoa Inportatu gabe Inportazio hutsegitea Geroago @@ -839,6 +893,7 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko Bcc Nori Nork + Erantzun honi <Hartzaile ezezaguna> <Bidaltzaile ezezaguna> Etxekoa @@ -977,9 +1032,14 @@ Mezu hau gorde dezakezu eta zure gako sekretuaren babes-kopia gisa erabili. Hau Akats bat gertatu da datuak kargatzean Hasieratzen… e-posta berrien zain + Lo, atzeko planoko sinkronizazioa baimendu arte + Lo, sarea eskuragarri izan arte Sakatu informazio gehiago lortzeko + Push Informazioa Push erabiltzean, K-9 Mailek posta zerbitzariarekiko konexioa mantentzen du. Androidek jakinarazpen bat erakutsi behar du, aplikazioa atzeko planoan martxan dagoen bitartean. %s Androidek ere jakinarazpena ezkutatzen uzten dizu. Informazio gehiago Konfiguratu jakinarazpena + Mezu berriei buruzko berehalako jakinarazpenik behar ez baduzu, Push desgaitu eta Bozketa erabili. Inkestak posta berriak egiaztatzen ditu aldizka eta ez du jakinarazpenik behar. + Desgaitu \"Push\"a diff --git a/app/ui/legacy/src/main/res/values-fa/strings.xml b/app/ui/legacy/src/main/res/values-fa/strings.xml index 86b5caa411..a8871947a5 100644 --- a/app/ui/legacy/src/main/res/values-fa/strings.xml +++ b/app/ui/legacy/src/main/res/values-fa/strings.xml @@ -12,6 +12,8 @@ مجوز آپاچی، نسخهٔ ۲٫۰ پروژهٔ متن‌باز وبگاه + راهنمای کاربر + کمک گرفتن انجمن کاربری فِدیوِرس توئیتر @@ -24,7 +26,7 @@ هنگامی که برنامه روزآمد شد تغییرات تازه را نشان بده از تازه‌های این نسخه باخبر شوید - به «کِی‌ناین میل» خوش آمدید + به «K-9 Mail» خوش آمدید ‏K-9 Mail برنامه‌ای رایگان برای مدیریت رایانامه در اندروید است. @@ -115,6 +117,7 @@ افزودن ستاره حذف ستاره کپی + لغو اشتراک نمایش سرایندها نشانی‌ها در بریده‌دان کپی شد @@ -166,7 +169,9 @@ احراز هویت ناموفق بود اعتبارسنجی %s ناموفق بود. تنظیمات کارساز خود را به‌روز کنید. + خطای اعلانها + هنگام ساخت هشدار برای پیام جدید سامانه خطایی رخ داد. اغلب خطا به علت عدم وجود آهنگ هشدارها است. \n\n برای بازکردن تنظیمات اعلانات، بزنید. به‌روزآوری رایانامه‌ها: %1$s:%2$s به‌روزآوری رایانامه‌ها ارسال رایانامه: %s @@ -280,7 +285,9 @@ گذرواژه جهت استفاده‌ این حساب کاربری از K-9 Mail، نیاز است که وارد حساب کاربری خود شوید و اجازه دسترسی اپلیکیشن به ایمیل‌ها را صادر کنید. + ورود + ورود با حساب گوگل برای دیدن گذرواژه‌تان در این قسمت، قفل صفحه در این دستگاه را فعال کنید. هویت‌تان را تایید کنید @@ -304,6 +311,7 @@ گذرواژه، به‌شکل ناامن ارسال شد گذرواژهٔ رمزنگاری‌شده گواهی کارخواه + OAuth 2.0 تنظیمات کارساز ورودی نام کاربری گذرواژه @@ -322,6 +330,7 @@ در کارساز پاک نشود در کارساز نیز پاک شود در کارساز نشان بزن که خواندم + استفاده از فشرده سازی پیام‌های حذف‌شده را در کارساز پاک کن فوراً در زمان سرکشی @@ -386,6 +395,10 @@ نام کاربری یا گذرواژه اشتباه است.\n(%s) کارساز یک گواهی SSL نامعتبر ارائه داده است. گاهی این مسئله به‌سبب پیکربندی نادرست کارساز است. گاهی اوقات علت آن است که فردی تلاش می‌کند به شما یا کارساز رایانامه حمله کند. اگر اطلاعات کافی ندارید، ردکردن را انتخاب کنید و با پشتیبانی رایانامهٔ خود تماس بگیرید.\n\n(%s) اتصال به کارساز ناموفق بود.\n(%s) + مجوزدهی لغو شد + مجوزدهی به علت این خطا ناتمام ماند: %s + OAuth 2.0 فعلا توسط این ارائه دهنده پشتیبانی نمی شود + برنامه نمی تواند مرورگری را پیدا کند تا دسترسی به حسابتان را بگیرد ویرایش جزئیات ادامه پیشرفته @@ -765,13 +778,16 @@ لطفاً گذرواژه‌ها را وارد کنید + لطفا وارد شوید + لطفا وارد شوید و رمز را وارد کنید درون‌برد تنظیمات ناموفق بود خواندن پروندهٔ تنظیمات ناموفق بود درون‌برد برخی تنظیمات ناموفق بود درون‌برد موفقیت‌آمیز بود گذرواژه لازم است + ورود نیاز است درون‌برد نشده شکست درون‌برد بعداً diff --git a/app/ui/legacy/src/main/res/values-hu/strings.xml b/app/ui/legacy/src/main/res/values-hu/strings.xml index 85f2362a40..b57ec77d29 100644 --- a/app/ui/legacy/src/main/res/values-hu/strings.xml +++ b/app/ui/legacy/src/main/res/values-hu/strings.xml @@ -12,6 +12,8 @@ Apache licenc, 2.0-s verzió Nyílt forráskódú projekt Weboldal + Felhasználói kézikönyv + Segítségkérés Felhasználói fórum Födiverzum Twitter @@ -103,6 +105,7 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd Keresés Keresés mindenhol Keresési eredmények + Új üzenetek Beállítások Mappák kezelése Fiók beállításai @@ -113,6 +116,7 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd Csillag hozzáadása Csillag eltávolítása Másolás + Leiratkozás Fejlécek megjelenítése Cím a vágólapra másolva @@ -157,12 +161,15 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd Archiválás Összes archiválása Levélszemét + Tanúsítványhiba Tanúsítványhiba: %s Ellenőrizze a kiszolgálóbeállításokat. A hitelesítés sikertelen. A hitelesítés sikertelen: %s. Frissítse a kiszolgálóbeállításokat. + Értesítési hiba + Hiba történt egy új üzenethez tartozó rendszerértesítés létrehozásának kísérletekor. Az ok valószínűleg egy hiányzó értesítési hang.\n\nKoppintson az értesítési beállítások megnyitásához. Levelek ellenőrzése: %1$s:%2$s Levelek ellenőrzése Levél küldése: %s @@ -185,6 +192,9 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd További diagnosztikai információk naplózása Érzékeny információk naplózása A jelszavak láthatóak lehetnek a naplókban. + Naplók exportálása + Az exportálás sikerült. A naplók érzékeny információkat tartalmazhatnak. Legyen óvatos, hogy kinek küldi el azokat. + Az exportálás sikertelen. További üzenetek betöltése Címzett:%s Tárgy @@ -231,6 +241,7 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd Címzettek neveinek használata a címjegyzékből, ha elérhető Partnerek színezése Nevek színezése a partnerlistában + Partner nevének színe Rögzített szélességű betűk Rögzített szélességű betű használata az egyszerű szöveges üzeneteknél Üzenetek automatikus méretezése @@ -270,8 +281,11 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd Új fiók beállítása E-mail-cím Jelszó + Ezen e-mail-fiók K-9 Maillel történő használatához be kell jelentkeznie, és hozzáférést kell adnia az alkalmazásnak az e-mailjeihez. + Bejelentkezés + Bejelentkezés Google használatával Ha itt szeretné megtekinteni jelszavát, engedélyezze a képernyőzárat ezen az eszközön. Személyazonosság ellenőrzése @@ -295,6 +309,7 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd Nem biztonságosan átküldött jelszó Titkosított jelszó Ügyféltanúsítvány + OAuth 2.0 Bejövő kiszolgáló beállításai Felhasználónév Jelszó @@ -313,6 +328,7 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd Nincs törlés a kiszolgálóról Törlés a kiszolgálóról Olvasottnak jelölés a kiszolgálón + Tömörítés használata Törölt üzenetek végleges törlése a kiszolgálóról Azonnal Lekérdezéskor @@ -377,6 +393,10 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd A felhasználónév vagy a jelszó hibás.\n(%s) A kiszolgáló érvénytelen SSL-tanúsítványt mutatott be. Néha ez a kiszolgáló hibás beállításából ered. Néha azért, mert valaki támadást hajtott végre Ön vagy a levelezőkiszolgálója ellen. Ha nem biztos abban, hogy mi történt, akkor kattintson az Elutasítás gombra, és vegye fel a kapcsolatot a levelezőkiszolgáló üzemeltetőivel.\n\n(%s) Nem lehet kapcsolódni a kiszolgálóhoz.\n(%s) + A felhatalmazás megszakítva + A felhatalmazás meghiúsult a következő hibával: %s + Az OAuth 2.0 jelenleg nem támogatott ezzel a szolgáltatóval. + Az alkalmazás nem talált böngészőt a fiókhoz történő hozzáférés megadásához való használathoz. Részletek szerkesztése Folytatás Bővített @@ -531,11 +551,29 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd Fióknév Saját név Értesítések + Rezgés Rezgés + Rezgés mintája Alapértelmezett + 1. minta + 2. minta + 3. minta + 4. minta + 5. minta Rezgés ismétlése + Letiltva Új levél csengőhangja + Értesítési fény + Letiltva Fiókszín + Rendszer alapértelmezett színe + Fehér + Piros + Zöld + Kék + Sárga + Ciánkék + Bíbor Üzenet írásának beállításai Írási alapbeállítások Alapértelmezett feladó, titkos másolat és aláírás beállítása @@ -738,13 +776,16 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd Adja meg a jelszavakat + Jelentkezzen be + Jelentkezzen be, és adja meg a jelszavakat Nem sikerült importálni a beállításokat Nem sikerült beolvasni a beállítások fájlt Nem sikerült importálni néhány beállítást Sikeresen importálva Jelszó szükséges + Bejelentkezés szükséges Nincs importálva Importálási hiba Később diff --git a/app/ui/legacy/src/main/res/values-pt-rBR/strings.xml b/app/ui/legacy/src/main/res/values-pt-rBR/strings.xml index faa31abe47..bbd9eb6719 100644 --- a/app/ui/legacy/src/main/res/values-pt-rBR/strings.xml +++ b/app/ui/legacy/src/main/res/values-pt-rBR/strings.xml @@ -173,8 +173,8 @@ Por favor encaminhe relatórios de bugs, contribua com novos recursos e tire dú Erro de notificação Ocorreu um erro ao tentar criar uma notificação de sistema para uma nova mensagem. O provável motivo é a ausência de um som de notificação.\n\nToque para abrir as configurações de notificação. - Verificando email: %1$s:%2$s - Verificando email + Verificando e-mail: %1$s:%2$s + Verificando e-mail Enviando mensagem: %s Enviando mensagem : @@ -261,7 +261,7 @@ Por favor encaminhe relatórios de bugs, contribua com novos recursos e tire dú Descartar mensagem Marcar todas as mensagens como lidas Excluir (na notificação) - Ocultar o cliente de email + Ocultar o cliente de e-mail Remove o User-Agent do K-9 dos cabeçalhos das mensagens Ocultar o fuso horário Usar UTC ao invés do fuso horário local nos cabeçalhos das mensagens e das respostas @@ -890,9 +890,9 @@ Por favor encaminhe relatórios de bugs, contribua com novos recursos e tire dú Usar certificado do cliente Nenhum certificado do cliente Remover seleção do certificado do cliente - Não foi possível recuperar o certificado do cliente para o apelido \"%s\" + Não foi possível obter o certificado do cliente para o apelido \"%s\" Opções avançadas - Certificado do cliente \"%1$s\" expirou ou não é mais válido (%2$s) + O certificado do cliente \"%1$s\" expirou ou ainda não é válido (%2$s) *Criptografado* Adicionar dos contatos diff --git a/app/ui/legacy/src/main/res/values-uk/strings.xml b/app/ui/legacy/src/main/res/values-uk/strings.xml index 87d41cbf78..b10ee2e88a 100644 --- a/app/ui/legacy/src/main/res/values-uk/strings.xml +++ b/app/ui/legacy/src/main/res/values-uk/strings.xml @@ -12,6 +12,8 @@ Ліцензія Apache версії 2.0 Проект з відкритим кодом Сайт + Інструкція користувача + Отримати допомогу Форум Соцмережі Twitter @@ -116,6 +118,7 @@ K-9 Mail — це вільний клієнт електронної пошти Додати зірочку Прибрати зірочку Копіювати + Відписатися Показати заголовки Адресу скопійовано до комірки обміну @@ -171,7 +174,9 @@ K-9 Mail — це вільний клієнт електронної пошти Помилка автентифікації Помилка автентифікації для %s. Змініть налаштування сервера. + Збій сповіщення + Під час створення системного сповіщення щодо нового повідомлення виникла помилка. Найчастіше це стається через відсутність звуку для сповіщення.\n\nТоркніться для переходу до налаштування сповіщення. Перевірка пошти: %1$s:%2$s Перевірка пошти Надсилання пошти: %s @@ -283,8 +288,11 @@ K-9 Mail — це вільний клієнт електронної пошти Налаштувати новий обліковий запис Адреса електронної пошти Пароль + Для використання цього поштового облікового запису у K-9 Mail Вам потрібно увійти в обліковий запис та надати доступ до ел.листів. + Увійти + Увійти з Google Щоб переглянути пароль, налаштуйте цьому пристрою блокування екрану. Звірте свою особу @@ -308,6 +316,7 @@ K-9 Mail — це вільний клієнт електронної пошти Пароль, що передається незахищено Зашифрований пароль Сертифікат клієнта + OAuth 2.0 Налаштування сервера вхідних повідомлень Ім’я користувача Пароль @@ -391,6 +400,10 @@ K-9 Mail — це вільний клієнт електронної пошти Ім\'я користувача або пароль невірні.\n(%s) Сервер надав недійсний сертифікат SSL. Це може бути зумовлено неналежним налаштуванням сервера або тим, що хтось намагається атакувати вас чи ваш поштовий сервер. Якщо ви не впевнені в тому, що відбувається, натисність "Відхилити" та зв’яжіться з адміністраторами вашого поштового серверу.\n\n(%s) Не можу з\'єднатися з сервером.\n(%s) + Авторизація скасована + Авторизація на вдалася з таких причин: %s + OAuth 2.0 наразі цим провайдером не підтримується. + Програма не може знайти браузер для надання доступу до Вашого облікового запису. Редагувати деталі Продовжити Додатково @@ -774,13 +787,16 @@ K-9 Mail — це вільний клієнт електронної пошти Введіть паролі + Увійдіть, будь ласка + Будь ласка, увійдіть та наберіть паролі Не вдалося імпортувати налаштування Не вдалося прочитати файл з налаштуваннями Не вдалося імпортувати деякі налаштування Успішно імпортовано Потрібен пароль + Потрібно увійти Не імпортовано Збій імпортування Пізніше diff --git a/app/ui/legacy/src/main/res/values-zh-rCN/strings.xml b/app/ui/legacy/src/main/res/values-zh-rCN/strings.xml index 51681ef9ba..7a6970834b 100644 --- a/app/ui/legacy/src/main/res/values-zh-rCN/strings.xml +++ b/app/ui/legacy/src/main/res/values-zh-rCN/strings.xml @@ -15,7 +15,7 @@ 用户手册 获取帮助 用户论坛 - 多样性 + Fediverse 推特 许可 diff --git a/app/ui/legacy/src/main/res/values-zh-rTW/strings.xml b/app/ui/legacy/src/main/res/values-zh-rTW/strings.xml index 750f1bd384..e310125842 100644 --- a/app/ui/legacy/src/main/res/values-zh-rTW/strings.xml +++ b/app/ui/legacy/src/main/res/values-zh-rTW/strings.xml @@ -167,7 +167,9 @@ K-9 Mail 是 Android 上一款功能強大,免費的電子郵件用戶端。 身份驗證失敗 %s 登入失敗。請更新你的伺服器設定。 + 通知錯誤 + 嘗試建立新訊息的系統通知時發生錯誤。最有可能的原因是缺少通知聲音。\n\n按一下開啟通知設定。 正在檢查郵件:%1$s:%2$s 正在檢查郵件 正在寄送郵件:%s -- GitLab From 944547a532e39d66cfb8608420edbc725f70ffbd Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 2 Sep 2022 17:06:03 +0200 Subject: [PATCH 65/85] Defer sorting opened messages to their new position in the message list 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, won't immediately change position in the message list if the list is sorted by these fields. The main benefit is that the swipe to next/previous message feature will work in a less surprising way. --- .../fsck/k9/fragment/MessageListFragment.kt | 40 ++++++++++++++++++- .../k9/ui/messagelist/MessageListConfig.kt | 8 +++- .../fsck/k9/ui/messagelist/MessageListItem.kt | 6 ++- .../k9/ui/messagelist/MessageListLoader.kt | 10 +++-- .../k9/ui/messagelist/MessageListViewModel.kt | 4 ++ .../MessageViewContainerFragment.kt | 3 -- 6 files changed, 61 insertions(+), 10 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt index b998ae2059..d27ff6752d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt @@ -52,6 +52,7 @@ import com.fsck.k9.ui.messagelist.MessageListConfig import com.fsck.k9.ui.messagelist.MessageListInfo import com.fsck.k9.ui.messagelist.MessageListItem import com.fsck.k9.ui.messagelist.MessageListViewModel +import com.fsck.k9.ui.messagelist.MessageSortOverride import java.util.HashSet import java.util.concurrent.Future import net.jcip.annotations.GuardedBy @@ -59,6 +60,8 @@ import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber +private const val MAXIMUM_MESSAGE_SORT_OVERRIDES = 3 + class MessageListFragment : Fragment(), OnItemClickListener, @@ -307,7 +310,8 @@ class MessageListFragment : sortType, sortAscending, sortDateAscending, - activeMessage + activeMessage, + viewModel.messageSortOverrides.toMap() ) viewModel.loadMessageList(config) } @@ -1554,6 +1558,8 @@ class MessageListFragment : fun setActiveMessage(messageReference: MessageReference?) { activeMessage = messageReference + rememberSortOverride(messageReference) + // Reload message list with modified query that always includes the active message if (isAdded) { loadMessageList() @@ -1570,6 +1576,38 @@ class MessageListFragment : } } + // 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, + // won't immediately change position in the message list if the list is sorted by these fields. + // The main benefit is that the swipe to next/previous message feature will work in a less surprising way. + private fun rememberSortOverride(messageReference: MessageReference?) { + val messageSortOverrides = viewModel.messageSortOverrides + + if (messageReference == null) { + messageSortOverrides.clear() + return + } + + if (sortType != SortType.SORT_UNREAD && sortType != SortType.SORT_FLAGGED) return + + val position = getPosition(messageReference) + val messageListItem = adapter.getItem(position) + + val existingEntry = messageSortOverrides.firstOrNull { it.first == messageReference } + if (existingEntry != null) { + messageSortOverrides.remove(existingEntry) + messageSortOverrides.addLast(existingEntry) + } else { + messageSortOverrides.addLast( + messageReference to MessageSortOverride(messageListItem.isRead, messageListItem.isStarred) + ) + if (messageSortOverrides.size > MAXIMUM_MESSAGE_SORT_OVERRIDES) { + messageSortOverrides.removeFirst() + } + } + } + private fun scrollToMessage(messageReference: MessageReference) { val position = getPosition(messageReference) val viewPosition = adapterToListViewPosition(position) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListConfig.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListConfig.kt index 3434954e37..8d8ad2d272 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListConfig.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListConfig.kt @@ -10,5 +10,11 @@ data class MessageListConfig( val sortType: SortType, val sortAscending: Boolean, val sortDateAscending: Boolean, - val activeMessage: MessageReference? + val activeMessage: MessageReference?, + val sortOverrides: Map +) + +data class MessageSortOverride( + val isRead: Boolean, + val isStarred: Boolean ) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt index 706225021d..4d7f67303f 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItem.kt @@ -1,6 +1,7 @@ package com.fsck.k9.ui.messagelist import com.fsck.k9.Account +import com.fsck.k9.controller.MessageReference import com.fsck.k9.mail.Address data class MessageListItem( @@ -25,4 +26,7 @@ data class MessageListItem( val messageUid: String, val databaseId: Long, val threadRoot: Long -) +) { + val messageReference: MessageReference + get() = MessageReference(account.uuid, folderId, messageUid) +} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt index 59dcdd2b3c..0409309cbf 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt @@ -136,12 +136,14 @@ class MessageListLoader( .thenByDate(config) } SortType.SORT_UNREAD -> { - compareBy(config.sortAscending) { it.isRead } - .thenByDate(config) + compareBy(config.sortAscending) { + config.sortOverrides[it.messageReference]?.isRead ?: it.isRead + }.thenByDate(config) } SortType.SORT_FLAGGED -> { - compareBy(!config.sortAscending) { it.isStarred } - .thenByDate(config) + compareBy(!config.sortAscending) { + config.sortOverrides[it.messageReference]?.isStarred ?: it.isStarred + }.thenByDate(config) } SortType.SORT_ATTACHMENT -> { compareBy(!config.sortAscending) { it.hasAttachments } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListViewModel.kt index e394fdd05c..253a18a7fc 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListViewModel.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListViewModel.kt @@ -4,11 +4,15 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.fsck.k9.controller.MessageReference +import java.util.LinkedList class MessageListViewModel(private val messageListLiveDataFactory: MessageListLiveDataFactory) : ViewModel() { private var currentMessageListLiveData: MessageListLiveData? = null private val messageListLiveData = MediatorLiveData() + val messageSortOverrides = LinkedList>() + fun getMessageListLiveData(): LiveData { return messageListLiveData } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt index 990a202de2..53d7592616 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt @@ -313,6 +313,3 @@ class MessageViewContainerFragment : Fragment() { } } } - -private val MessageListItem.messageReference: MessageReference - get() = MessageReference(account.uuid, folderId, messageUid) -- GitLab From 0286afa6c82b82fbe1d2385ce8d3c3edcc9a36e2 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 5 Sep 2022 13:33:10 +0200 Subject: [PATCH 66/85] Version 6.302 --- app/k9mail/build.gradle | 4 ++-- app/ui/legacy/src/main/res/raw/changelog_master.xml | 7 +++++++ fastlane/metadata/android/en-US/changelogs/33002.txt | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/33002.txt diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 8e701962a0..4e466829d8 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -48,8 +48,8 @@ android { applicationId "com.fsck.k9" testApplicationId "com.fsck.k9.tests" - versionCode 33001 - versionName '6.302-SNAPSHOT' + versionCode 33002 + versionName '6.302' // 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", diff --git a/app/ui/legacy/src/main/res/raw/changelog_master.xml b/app/ui/legacy/src/main/res/raw/changelog_master.xml index 698125bb54..4c8db3e7b1 100644 --- a/app/ui/legacy/src/main/res/raw/changelog_master.xml +++ b/app/ui/legacy/src/main/res/raw/changelog_master.xml @@ -5,6 +5,13 @@ Locale-specific versions are kept in res/raw-/changelog.xml. --> + + Fixed moving to next/previous message when sorting the message list by read/unread or starred/unstarred + Fixed a crash when a third-party app shared a file to K-9 Mail without granting access to it + Keep some more attributes when sanitizing HTML + A lot of internal changes and improvements + Updated translations + Fixed crash when viewing a message and OpenKeychain needed to display its user interface, e.g. to ask for a password When composing a message containing consecutive spaces convert them to non-breaking spaces in the generated HTML part of the message diff --git a/fastlane/metadata/android/en-US/changelogs/33002.txt b/fastlane/metadata/android/en-US/changelogs/33002.txt new file mode 100644 index 0000000000..3cb99e7ca0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/33002.txt @@ -0,0 +1,5 @@ +- Fixed moving to next/previous message when sorting the message list by read/unread or starred/unstarred +- Fixed a crash when a third-party app shared a file to K-9 Mail without granting access to it +- Keep some more attributes when sanitizing HTML +- A lot of internal changes and improvements +- Updated translations -- GitLab From cd95aa233bbc039b5c6a601ec827e9737fc8f580 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 5 Sep 2022 13:58:22 +0200 Subject: [PATCH 67/85] Prepare for version 6.303 --- app/k9mail/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 4e466829d8..510c9b8784 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -49,7 +49,7 @@ android { testApplicationId "com.fsck.k9.tests" versionCode 33002 - versionName '6.302' + versionName '6.303-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", -- GitLab From 32dd66973910a4987631d66af7f5448f8331611a Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 5 Sep 2022 14:47:11 +0200 Subject: [PATCH 68/85] Add comment explaining why we currently can't upgrade Commons IO --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 8e5d1bee6b..016fa688cb 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,9 @@ buildscript { 'moshi': '1.13.0', 'timber': '5.0.1', 'koin': '3.2.0', + // 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', -- GitLab From bf2ac0b645fc4c2b1031f3b5d196f854d30f4f7e Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 5 Sep 2022 15:27:05 +0200 Subject: [PATCH 69/85] Add a note on upgrading dependencies --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 016fa688cb..71142b8c6a 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,8 @@ buildscript { 'robolectricSdk': 31 ] + // 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.10', 'kotlinCoroutines': '1.6.4', -- GitLab From a9030a76a5a6b0fcd3cecd99b0d9615c0b756ea3 Mon Sep 17 00:00:00 2001 From: PatrickMis <24607131+PatrickMis@users.noreply.github.com> Date: Mon, 5 Sep 2022 02:57:01 +0200 Subject: [PATCH 70/85] Upgrade ktlint to 0.44.0 --- .editorconfig | 2 +- build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index b759ad0601..73a670c902 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,2 +1,2 @@ [*.{kt,kts}] -kotlin_imports_layout = *,^* +ij_kotlin_imports_layout = *,^* diff --git a/build.gradle b/build.gradle index 71142b8c6a..cc2959f90d 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ buildscript { 'truth': '1.1.3', 'turbine': '0.9.0', - 'ktlint': '0.40.0' + 'ktlint': '0.44.0' ] javaVersion = JavaVersion.VERSION_1_8 @@ -72,7 +72,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" - classpath "org.jlleitschuh.gradle:ktlint-gradle:10.0.0" + classpath "org.jlleitschuh.gradle:ktlint-gradle:11.0.0" } } -- GitLab From dfec81c8778407d398739cf4c34f8f80020d5731 Mon Sep 17 00:00:00 2001 From: PatrickMis <24607131+PatrickMis@users.noreply.github.com> Date: Mon, 5 Sep 2022 02:56:21 +0200 Subject: [PATCH 71/85] Make ktlint happy --- .../app/k9mail/html/cleaner/BodyCleaner.kt | 1 - .../fsck/k9/message/PgpMessageBuilderTest.kt | 2 +- .../transport/smtp/SmtpResponseParserTest.kt | 28 +++++++++---------- .../mail/transport/smtp/SmtpTransportTest.kt | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt index 5697b115c4..3bc44effdc 100644 --- a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt @@ -38,7 +38,6 @@ internal class BodyCleaner { .addAttributes("img", "usemap") .addAttributes(":all", "class", "style", "id", "dir") .addProtocols("img", "src", "http", "https", "cid", "data") - // Allow all URI schemes in links .removeProtocols("a", "href", "ftp", "http", "https", "mailto") diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt index c4887fb772..a2c91597ee 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt @@ -46,11 +46,11 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.ArgumentMatchers.same -import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` import org.mockito.kotlin.anyOrNull import org.openintents.openpgp.OpenPgpApiManager.OpenPgpProviderState import org.openintents.openpgp.OpenPgpError diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt index 9b813c0ae5..a9d4d3e62b 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt @@ -27,7 +27,7 @@ class SmtpResponseParserTest { val input = """ 220-Greetings, stranger 220 smtp.domain.example ESMTP ready - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) val response = parser.readGreeting() @@ -125,7 +125,7 @@ class SmtpResponseParserTest { val input = """ 250-smtp.domain.example 220 - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) assertFailsWithMessage("Multi-line response with reply codes not matching: 250 != 220") { @@ -154,7 +154,7 @@ class SmtpResponseParserTest { 250-PIPE_CONNECT 250-AUTH=PLAIN 250 HELP - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) val response = parser.readHelloResponse() @@ -183,7 +183,7 @@ class SmtpResponseParserTest { val input = """ 250-smtp.domain.example 250 KEYWORD${" "} - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) val response = parser.readHelloResponse() @@ -203,7 +203,7 @@ class SmtpResponseParserTest { 250-smtp.domain.example 250-8BITMIME 250 KEYWORD para${"\t"}meter - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) val response = parser.readHelloResponse() @@ -298,7 +298,7 @@ class SmtpResponseParserTest { val input = """ 500-Line one 500 Line two - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) val response = parser.readResponse(enhancedStatusCodes = false) @@ -314,7 +314,7 @@ class SmtpResponseParserTest { val input = """ 500- 500 Line two - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) val response = parser.readResponse(enhancedStatusCodes = false) @@ -331,7 +331,7 @@ class SmtpResponseParserTest { 500-Line one 500-Line two 500 - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) val response = parser.readResponse(enhancedStatusCodes = false) @@ -347,7 +347,7 @@ class SmtpResponseParserTest { val input = """ 250-2.1.0 Sender 250 2.1.0 OK - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) val response = parser.readResponse(enhancedStatusCodes = true) @@ -365,7 +365,7 @@ class SmtpResponseParserTest { val input = """ 250 Sender OK 250 Recipient OK - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) val responseOne = parser.readResponse(enhancedStatusCodes = false) @@ -387,7 +387,7 @@ class SmtpResponseParserTest { val input = """ 200-Line one 500 Line two - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) assertFailsWithMessage("Multi-line response with reply codes not matching: 200 != 500") { @@ -411,7 +411,7 @@ class SmtpResponseParserTest { val input = """ 200-Line one 500 Line two - """.toPeekableInputStream() + """.toPeekableInputStream() val logger = TestSmtpLogger(isRawProtocolLoggingEnabled = false) val parser = SmtpResponseParser(logger, input) @@ -627,7 +627,7 @@ class SmtpResponseParserTest { val input = """ 550-5.2.1 Request failed 550 Mailbox unavailable - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) assertFailsWithMessage( @@ -643,7 +643,7 @@ class SmtpResponseParserTest { val input = """ 550-Request failed 550 Mailbox unavailable - """.toPeekableInputStream() + """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) val response = parser.readResponse(enhancedStatusCodes = true) diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt index a0c135f6ff..bc8cd8840d 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt @@ -730,7 +730,7 @@ class SmtpTransportTest { assertThat(e).hasMessageThat().isEqualTo("Message too large for server") } - // FIXME: Make sure connection was closed + // FIXME: Make sure connection was closed // server.verifyConnectionClosed(); } -- GitLab From 09e587b403be085afb2d19895e4e2961aecf58df Mon Sep 17 00:00:00 2001 From: PatrickMis <24607131+PatrickMis@users.noreply.github.com> Date: Sun, 4 Sep 2022 19:35:12 +0200 Subject: [PATCH 72/85] Update Gradle Wrapper --- gradle/wrapper/gradle-wrapper.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ec77e51a9..f7189a776c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -- GitLab From f94e55398700a41f9e32bb810b4b3a3535941fa6 Mon Sep 17 00:00:00 2001 From: PatrickMis <24607131+PatrickMis@users.noreply.github.com> Date: Sun, 4 Sep 2022 20:37:41 +0200 Subject: [PATCH 73/85] Bump CompileSDK and AndroidX dependencies --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index cc2959f90d..b60b683cce 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinCompile buildscript { ext { buildConfig = [ - 'compileSdk': 31, + 'compileSdk': 33, 'targetSdk': 31, 'minSdk': 21, 'buildTools': '32.0.0', @@ -16,7 +16,7 @@ buildscript { 'kotlin': '1.7.10', 'kotlinCoroutines': '1.6.4', 'jetbrainsAnnotations': '23.0.0', - 'androidxAppCompat': '1.4.2', + 'androidxAppCompat': '1.5.0', 'androidxActivity': '1.5.1', 'androidxRecyclerView': '1.2.1', 'androidxLifecycle': '2.5.1', @@ -25,7 +25,7 @@ buildscript { 'androidxNavigation': '2.5.1', 'androidxConstraintLayout': '2.1.4', 'androidxWorkManager': '2.7.1', - 'androidxFragment': '1.5.1', + 'androidxFragment': '1.5.2', 'androidxLocalBroadcastManager': '1.1.0', 'androidxCore': '1.8.0', 'androidxCardView': '1.0.0', -- GitLab From fb44ff8f1ff4c5ac8448309814f6571217d01fe4 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Sep 2022 13:07:44 +0200 Subject: [PATCH 74/85] Rename .java to .kt --- .../{HtmlSignatureRemover.java => HtmlSignatureRemover.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/core/src/main/java/com/fsck/k9/message/signature/{HtmlSignatureRemover.java => HtmlSignatureRemover.kt} (100%) diff --git a/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.java b/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.kt similarity index 100% rename from app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.java rename to app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.kt -- GitLab From e74f0a5780ed170fecc22f564459f4e80ce718d9 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Sep 2022 13:07:44 +0200 Subject: [PATCH 75/85] Convert `HtmlSignatureRemover` to Kotlin --- .../message/signature/HtmlSignatureRemover.kt | 153 ++++++++---------- 1 file changed, 69 insertions(+), 84 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.kt b/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.kt index 175623b05d..72900cd88c 100644 --- a/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.kt +++ b/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.kt @@ -1,106 +1,91 @@ -package com.fsck.k9.message.signature; - - -import java.util.regex.Pattern; - -import androidx.annotation.NonNull; -import com.fsck.k9.helper.jsoup.AdvancedNodeTraversor; -import com.fsck.k9.helper.jsoup.NodeFilter; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.nodes.Node; -import org.jsoup.nodes.TextNode; -import org.jsoup.parser.Tag; - - -public class HtmlSignatureRemover { - public static String stripSignature(String content) { - return new HtmlSignatureRemover().stripSignatureInternal(content); +package com.fsck.k9.message.signature + +import com.fsck.k9.helper.jsoup.AdvancedNodeTraversor +import com.fsck.k9.helper.jsoup.NodeFilter +import com.fsck.k9.helper.jsoup.NodeFilter.HeadFilterDecision +import com.fsck.k9.helper.jsoup.NodeFilter.TailFilterDecision +import java.util.regex.Pattern +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode +import org.jsoup.parser.Tag + +class HtmlSignatureRemover { + private fun stripSignatureInternal(content: String): String { + val document = Jsoup.parse(content) + + val nodeTraversor = AdvancedNodeTraversor(StripSignatureFilter()) + nodeTraversor.filter(document.body()) + + return toCompactString(document) } - private String stripSignatureInternal(String content) { - Document document = Jsoup.parse(content); - - AdvancedNodeTraversor nodeTraversor = new AdvancedNodeTraversor(new StripSignatureFilter()); - nodeTraversor.filter(document.body()); - - return toCompactString(document); - } - - private String toCompactString(Document document) { + private fun toCompactString(document: Document): String { document.outputSettings() - .prettyPrint(false) - .indentAmount(0); + .prettyPrint(false) + .indentAmount(0) - return document.html(); + return document.html() } + private class StripSignatureFilter : NodeFilter { + private var signatureFound = false + private var lastElementCausedLineBreak = false + private var brElementPrecedingDashes: Element? = null - static class StripSignatureFilter implements NodeFilter { - private static final Pattern DASH_SIGNATURE_HTML = Pattern.compile("\\s*-- \\s*", Pattern.CASE_INSENSITIVE); - private static final Tag BLOCKQUOTE = Tag.valueOf("blockquote"); - private static final Tag BR = Tag.valueOf("br"); - private static final Tag P = Tag.valueOf("p"); - - - private boolean signatureFound = false; - private boolean lastElementCausedLineBreak = false; - private Element brElementPrecedingDashes; + override fun head(node: Node, depth: Int): HeadFilterDecision { + if (signatureFound) return HeadFilterDecision.REMOVE - - @NonNull - @Override - public HeadFilterDecision head(Node node, int depth) { - if (signatureFound) { - return HeadFilterDecision.REMOVE; - } - - if (node instanceof Element) { - lastElementCausedLineBreak = false; - - Element element = (Element) node; - if (element.tag().equals(BLOCKQUOTE)) { - return HeadFilterDecision.SKIP_ENTIRELY; + if (node is Element) { + lastElementCausedLineBreak = false + if (node.tag() == BLOCKQUOTE) { + return HeadFilterDecision.SKIP_ENTIRELY } - } else if (node instanceof TextNode) { - TextNode textNode = (TextNode) node; - if (lastElementCausedLineBreak && DASH_SIGNATURE_HTML.matcher(textNode.getWholeText()).matches()) { - Node nextNode = node.nextSibling(); - if (nextNode instanceof Element && ((Element) nextNode).tag().equals(BR)) { - signatureFound = true; - if (brElementPrecedingDashes != null) { - brElementPrecedingDashes.remove(); - brElementPrecedingDashes = null; - } - - return HeadFilterDecision.REMOVE; + } else if (node is TextNode) { + if (lastElementCausedLineBreak && DASH_SIGNATURE_HTML.matcher(node.wholeText).matches()) { + val nextNode = node.nextSibling() + if (nextNode is Element && nextNode.tag() == BR) { + signatureFound = true + brElementPrecedingDashes?.remove() + brElementPrecedingDashes = null + + return HeadFilterDecision.REMOVE } } } - return HeadFilterDecision.CONTINUE; + return HeadFilterDecision.CONTINUE } - @NonNull - @Override - public TailFilterDecision tail(Node node, int depth) { - if (signatureFound) { - return TailFilterDecision.CONTINUE; - } + override fun tail(node: Node, depth: Int): TailFilterDecision { + if (signatureFound) return TailFilterDecision.CONTINUE - if (node instanceof Element) { - Element element = (Element) node; - boolean elementIsBr = element.tag().equals(BR); - if (elementIsBr || element.tag().equals(P)) { - lastElementCausedLineBreak = true; - brElementPrecedingDashes = elementIsBr ? element : null; - return TailFilterDecision.CONTINUE; + if (node is Element) { + val elementIsBr = node.tag() == BR + if (elementIsBr || node.tag() == P) { + lastElementCausedLineBreak = true + brElementPrecedingDashes = if (elementIsBr) node else null + + return TailFilterDecision.CONTINUE } } - lastElementCausedLineBreak = false; - return TailFilterDecision.CONTINUE; + lastElementCausedLineBreak = false + return TailFilterDecision.CONTINUE + } + } + + companion object { + private val DASH_SIGNATURE_HTML = Pattern.compile("\\s*-- \\s*", Pattern.CASE_INSENSITIVE) + private val BLOCKQUOTE = Tag.valueOf("blockquote") + private val BR = Tag.valueOf("br") + private val P = Tag.valueOf("p") + + @JvmStatic + fun stripSignature(content: String): String { + return HtmlSignatureRemover().stripSignatureInternal(content) } } } -- GitLab From 13a83d0be063e4c9e1d453c0c81790680e3c7c5c Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Sep 2022 13:36:33 +0200 Subject: [PATCH 76/85] Rename .java to .kt --- ...{HtmlSignatureRemoverTest.java => HtmlSignatureRemoverTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/core/src/test/java/com/fsck/k9/message/signature/{HtmlSignatureRemoverTest.java => HtmlSignatureRemoverTest.kt} (100%) diff --git a/app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.java b/app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.kt similarity index 100% rename from app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.java rename to app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.kt -- GitLab From 451f4df4b28f67d85f1b32eced7ed172a049b764 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Sep 2022 13:36:33 +0200 Subject: [PATCH 77/85] Convert `HtmlSignatureRemoverTest` to Kotlin --- .../signature/HtmlSignatureRemoverTest.kt | 248 +++++++++--------- .../com/fsck/k9/testing/StringExtensions.kt | 3 + 2 files changed, 132 insertions(+), 119 deletions(-) create mode 100644 app/testing/src/main/java/com/fsck/k9/testing/StringExtensions.kt diff --git a/app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.kt b/app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.kt index bf5f606860..f6370c5273 100644 --- a/app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.kt +++ b/app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.kt @@ -1,141 +1,151 @@ -package com.fsck.k9.message.signature; +package com.fsck.k9.message.signature +import com.fsck.k9.message.html.HtmlHelper.extractText +import com.fsck.k9.message.signature.HtmlSignatureRemover.Companion.stripSignature +import com.fsck.k9.testing.removeNewlines +import com.google.common.truth.Truth.assertThat +import org.junit.Test -import org.junit.Test; - -import static com.fsck.k9.message.html.HtmlHelper.extractText; -import static org.junit.Assert.assertEquals; - - -public class HtmlSignatureRemoverTest { +class HtmlSignatureRemoverTest { @Test - public void shouldStripSignatureFromK9StyleHtml() throws Exception { - String html = "This is the body text" + - "
" + - "--
" + - "Sent from my Android device with K-9 Mail. Please excuse my brevity."; + fun `old K-9 Mail signature format`() { + val html = + """This is the body text
--
Sent from my Android device with K-9 Mail. Please excuse my brevity.""" - String withoutSignature = HtmlSignatureRemover.stripSignature(html); + val withoutSignature = stripSignature(html) - assertEquals("This is the body text", extractText(withoutSignature)); + assertThat(extractText(withoutSignature)).isEqualTo("This is the body text") } @Test - public void shouldStripSignatureFromThunderbirdStyleHtml() throws Exception { - String html = "\r\n" + - " \r\n" + - " \r\n" + - " \r\n" + - " \r\n" + - "

This is the body text
\r\n" + - "

\r\n" + - " --
\r\n" + - "
Sent from my Android device with K-9 Mail." + - " Please excuse my brevity.
\r\n" + - " \r\n" + - ""; - - String withoutSignature = HtmlSignatureRemover.stripSignature(html); - - assertEquals("This is the body text", extractText(withoutSignature)); + fun `old Thunderbird signature format`() { + val html = + """ + + + + + +

This is the body text
+

+ --
+
Sent from my Android device with K-9 Mail. Please excuse my brevity.
+ + + """.trimIndent() + + val withoutSignature = stripSignature(html) + + assertThat(extractText(withoutSignature)).isEqualTo("This is the body text") } @Test - public void shouldStripSignatureBeforeBlockquoteTag() throws Exception { - String html = "" + - "
" + - "This is the body text" + - "
" + - "--
" + - "
" + - "Sent from my Android device with K-9 Mail. Please excuse my brevity." + - "
" + - "
" + - ""; - - String withoutSignature = HtmlSignatureRemover.stripSignature(html); - - assertEquals("" + - "
This is the body text
" + - "", - withoutSignature); + fun `signature before blockquote tag`() { + val html = + """ + + + +
+ This is the body text
+ --
+
Sent from my Android device with K-9 Mail. Please excuse my brevity.
+
+ + + """.trimIndent().removeNewlines() + + val withoutSignature = stripSignature(html) + + assertThat(withoutSignature).isEqualTo( + """
This is the body text
""" + ) } @Test - public void shouldNotStripSignatureInsideBlockquoteTags() throws Exception { - String html = "" + - "
" + - "This is some quoted text" + - "
" + - "--
" + - "Inner signature" + - "
" + - "
" + - "This is the body text" + - "
" + - ""; - - String withoutSignature = HtmlSignatureRemover.stripSignature(html); - - assertEquals("" + - "
" + - "This is some quoted text" + - "
" + - "--
" + - "Inner signature" + - "
" + - "
This is the body text
" + - "", - withoutSignature); + fun `should not strip signature inside blockquote tag`() { + val html = + """ + + + +
+ This is some quoted text
+ --
+ Inner signature +
+
+ This is the body text +
+ + + """.trimIndent().removeNewlines() + + val withoutSignature = stripSignature(html) + + assertThat(withoutSignature).isEqualTo(html) } @Test - public void shouldStripSignatureBetweenBlockquoteTags() throws Exception { - String html = "" + - "
" + - "Some quote" + - "
" + - "
" + - "This is the body text" + - "
" + - "--
" + - "
" + - "Sent from my Android device with K-9 Mail. Please excuse my brevity." + - "
" + - "
" + - "--
" + - "Signature inside signature" + - "
" + - ""; - - String withoutSignature = HtmlSignatureRemover.stripSignature(html); - - assertEquals("" + - "
Some quote
" + - "
This is the body text
" + - "", - withoutSignature); + fun `signature between blockquote tags`() { + val html = + """ + + + +
Some quote
+
This is the body text
+ --
+
Sent from my Android device with K-9 Mail. Please excuse my brevity.
+
--
Signature inside signature +
+ + + """.trimIndent().removeNewlines() + + val withoutSignature = stripSignature(html) + + assertThat(withoutSignature).isEqualTo( + """ + + + +
Some quote
+
This is the body text
+ + + """.trimIndent().removeNewlines() + ) } @Test - public void shouldStripSignatureAfterLastBlockquoteTags() throws Exception { - String html = "" + - "This is the body text" + - "
" + - "
" + - "Some quote" + - "
" + - "
" + - "--
" + - "Sent from my Android device with K-9 Mail. Please excuse my brevity." + - ""; - - String withoutSignature = HtmlSignatureRemover.stripSignature(html); - - assertEquals("" + - "This is the body text
" + - "
Some quote
" + - "", - withoutSignature); + fun `signature after last blockquote tag`() { + val html = + """ + + + + This is the body text
+
Some quote
+
+ --
+ Sent from my Android device with K-9 Mail. Please excuse my brevity. + + + """.trimIndent().removeNewlines() + + val withoutSignature = stripSignature(html) + + assertThat(withoutSignature).isEqualTo( + """ + + + + This is the body text
+
Some quote
+ + + """.trimIndent().removeNewlines() + ) } } diff --git a/app/testing/src/main/java/com/fsck/k9/testing/StringExtensions.kt b/app/testing/src/main/java/com/fsck/k9/testing/StringExtensions.kt new file mode 100644 index 0000000000..2e5a461c19 --- /dev/null +++ b/app/testing/src/main/java/com/fsck/k9/testing/StringExtensions.kt @@ -0,0 +1,3 @@ +package com.fsck.k9.testing + +fun String.removeNewlines(): String = replace("([\\r\\n])".toRegex(), "") -- GitLab From 2fe289875e3ab062fd33387c1316c94aef1417bf Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Sep 2022 16:22:44 +0200 Subject: [PATCH 78/85] Add support for removing K-9 Mail signatures from HTML message parts --- .../helper/jsoup/AdvancedNodeTraversor.java | 2 +- .../message/signature/HtmlSignatureRemover.kt | 105 +++++++++++++----- .../signature/HtmlSignatureRemoverTest.kt | 33 ++++++ 3 files changed, 112 insertions(+), 28 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/helper/jsoup/AdvancedNodeTraversor.java b/app/core/src/main/java/com/fsck/k9/helper/jsoup/AdvancedNodeTraversor.java index cfcab37a4c..41cd5e245f 100644 --- a/app/core/src/main/java/com/fsck/k9/helper/jsoup/AdvancedNodeTraversor.java +++ b/app/core/src/main/java/com/fsck/k9/helper/jsoup/AdvancedNodeTraversor.java @@ -125,7 +125,7 @@ public class AdvancedNodeTraversor { Node prev = node; node = node.nextSibling(); - if (headResult == HeadFilterDecision.REMOVE) { + if (headResult == HeadFilterDecision.REMOVE || tailResult == TailFilterDecision.REMOVE) { prev.remove(); } diff --git a/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.kt b/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.kt index 72900cd88c..07746c20dc 100644 --- a/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.kt +++ b/app/core/src/main/java/com/fsck/k9/message/signature/HtmlSignatureRemover.kt @@ -4,6 +4,7 @@ import com.fsck.k9.helper.jsoup.AdvancedNodeTraversor import com.fsck.k9.helper.jsoup.NodeFilter import com.fsck.k9.helper.jsoup.NodeFilter.HeadFilterDecision import com.fsck.k9.helper.jsoup.NodeFilter.TailFilterDecision +import java.util.Stack import java.util.regex.Pattern import org.jsoup.Jsoup import org.jsoup.nodes.Document @@ -32,27 +33,21 @@ class HtmlSignatureRemover { private class StripSignatureFilter : NodeFilter { private var signatureFound = false - private var lastElementCausedLineBreak = false - private var brElementPrecedingDashes: Element? = null + private var signatureParentNode: Node? = null override fun head(node: Node, depth: Int): HeadFilterDecision { if (signatureFound) return HeadFilterDecision.REMOVE - if (node is Element) { - lastElementCausedLineBreak = false - if (node.tag() == BLOCKQUOTE) { - return HeadFilterDecision.SKIP_ENTIRELY - } - } else if (node is TextNode) { - if (lastElementCausedLineBreak && DASH_SIGNATURE_HTML.matcher(node.wholeText).matches()) { - val nextNode = node.nextSibling() - if (nextNode is Element && nextNode.tag() == BR) { - signatureFound = true - brElementPrecedingDashes?.remove() - brElementPrecedingDashes = null - - return HeadFilterDecision.REMOVE - } + if (node.isBlockquote()) { + return HeadFilterDecision.SKIP_ENTIRELY + } else if (node.isSignatureDelimiter()) { + val precedingLineBreak = node.findPrecedingLineBreak() + if (precedingLineBreak != null && node.isFollowedByLineBreak()) { + signatureFound = true + signatureParentNode = node.parent() + precedingLineBreak.takeIf { it.isBR() }?.remove() + + return HeadFilterDecision.REMOVE } } @@ -60,28 +55,84 @@ class HtmlSignatureRemover { } override fun tail(node: Node, depth: Int): TailFilterDecision { - if (signatureFound) return TailFilterDecision.CONTINUE + if (signatureFound) { + val signatureParentNode = this.signatureParentNode + if (node == signatureParentNode) { + return if (signatureParentNode.isEmpty()) { + this.signatureParentNode = signatureParentNode.parent() + TailFilterDecision.REMOVE + } else { + TailFilterDecision.STOP + } + } + } - if (node is Element) { - val elementIsBr = node.tag() == BR - if (elementIsBr || node.tag() == P) { - lastElementCausedLineBreak = true - brElementPrecedingDashes = if (elementIsBr) node else null + return TailFilterDecision.CONTINUE + } + + private fun Node.isBlockquote(): Boolean { + return this is Element && tag() == BLOCKQUOTE + } + + private fun Node.isSignatureDelimiter(): Boolean { + return this is TextNode && DASH_SIGNATURE_HTML.matcher(wholeText).matches() + } - return TailFilterDecision.CONTINUE + private fun Node.findPrecedingLineBreak(): Node? { + val stack = Stack() + stack.push(this) + + while (stack.isNotEmpty()) { + val node = stack.pop() + val previousSibling = node.previousSibling() + if (previousSibling == null) { + val parent = node.parent() + if (parent is Element && parent.isBlock) { + return parent + } else { + stack.push(parent) + } + } else if (previousSibling.isLineBreak()) { + return previousSibling } } - lastElementCausedLineBreak = false - return TailFilterDecision.CONTINUE + return null } + + private fun Node.isFollowedByLineBreak(): Boolean { + val stack = Stack() + stack.push(this) + + while (stack.isNotEmpty()) { + val node = stack.pop() + val nextSibling = node.nextSibling() + if (nextSibling == null) { + val parent = node.parent() + if (parent is Element && parent.isBlock) { + return true + } else { + stack.push(parent) + } + } else if (nextSibling.isLineBreak()) { + return true + } + } + + return false + } + + private fun Node?.isBR() = this is Element && tag() == BR + + private fun Node?.isLineBreak() = isBR() || (this is Element && this.isBlock) + + private fun Node.isEmpty(): Boolean = childNodeSize() == 0 } companion object { private val DASH_SIGNATURE_HTML = Pattern.compile("\\s*-- \\s*", Pattern.CASE_INSENSITIVE) private val BLOCKQUOTE = Tag.valueOf("blockquote") private val BR = Tag.valueOf("br") - private val P = Tag.valueOf("p") @JvmStatic fun stripSignature(content: String): String { diff --git a/app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.kt b/app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.kt index f6370c5273..f9ca1af7d4 100644 --- a/app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.kt +++ b/app/core/src/test/java/com/fsck/k9/message/signature/HtmlSignatureRemoverTest.kt @@ -148,4 +148,37 @@ class HtmlSignatureRemoverTest { """.trimIndent().removeNewlines() ) } + + @Test + fun `K-9 Mail signature format`() { + val html = + """ + + + + This is the body text.
+
+
+ --
+ And this is the signature text. +
+ + + """.trimIndent().removeNewlines() + + val withoutSignature = stripSignature(html) + + assertThat(withoutSignature).isEqualTo( + """ + + + + + This is the body text.
+
+ + + """.trimIndent().removeNewlines() + ) + } } -- GitLab From 2c94dc6d08d1948929b42ab5dd4ec8d00a587763 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Sep 2022 17:10:05 +0200 Subject: [PATCH 79/85] Use proper table name in `SqlQueryBuilder` --- app/core/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java | 2 +- .../fsck/k9/storage/messages/RetrieveMessageListOperations.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java b/app/core/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java index 06b14bef87..4ce3bcc9a3 100644 --- a/app/core/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java +++ b/app/core/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java @@ -66,7 +66,7 @@ public class SqlQueryBuilder { if (condition.attribute != Attribute.CONTAINS) { Timber.e("message contents can only be matched!"); } - query.append("m.id IN (SELECT docid FROM messages_fulltext WHERE fulltext MATCH ?)"); + query.append("messages.id IN (SELECT docid FROM messages_fulltext WHERE fulltext MATCH ?)"); selectionArgs.add(fulltextQueryString); break; } diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt index 2d01b18305..45d8b4340c 100644 --- a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt +++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageListOperations.kt @@ -1,7 +1,6 @@ package com.fsck.k9.storage.messages import android.database.Cursor -import com.fsck.k9.helper.map import com.fsck.k9.mail.Address import com.fsck.k9.mailstore.DatabasePreviewType import com.fsck.k9.mailstore.LockableDatabase -- GitLab From 53a08bcdee8ee4e6e6356b8b5411d45e62e559f6 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Sep 2022 17:44:08 +0200 Subject: [PATCH 80/85] Fix mechanism to notify about messages that failed to send --- .../fsck/k9/controller/MessagingController.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java index 2cf0e69e4f..37986e3f7d 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -1551,10 +1551,16 @@ public class MessagingController { long messageId = message.getDatabaseId(); OutboxState outboxState = outboxStateRepository.getOutboxState(messageId); - if (outboxState.getSendState() != SendState.READY) { - Timber.v("Skipping sending message %s", message.getUid()); - notificationController.showSendFailedNotification(account, - new MessagingException(message.getSubject())); + SendState sendState = outboxState.getSendState(); + if (sendState != SendState.READY) { + Timber.v("Skipping sending message %s (reason: %s)", message.getUid(), + sendState.getDatabaseName()); + + if (sendState == SendState.RETRIES_EXCEEDED) { + lastFailure = new MessagingException("Retries exceeded", true); + } else { + lastFailure = new MessagingException(outboxState.getSendError(), true); + } continue; } -- GitLab From c80254f3a56fa816135b2dff8424d522f4ab6ffd Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Sep 2022 17:45:21 +0200 Subject: [PATCH 81/85] Remove unnecessary code --- .../fsck/k9/controller/MessagingController.java | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java index 37986e3f7d..03de5e215c 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -1501,7 +1501,6 @@ public class MessagingController { @VisibleForTesting protected void sendPendingMessagesSynchronous(final Account account) { Exception lastFailure = null; - boolean wasPermanentFailure = false; try { if (isAuthenticationProblem(account, false)) { Timber.d("Authentication will fail. Skip sending messages."); @@ -1593,22 +1592,19 @@ public class MessagingController { } catch (AuthenticationFailedException e) { outboxStateRepository.decrementSendAttempts(messageId); lastFailure = e; - wasPermanentFailure = false; handleAuthenticationFailure(account, false); handleSendFailure(account, localFolder, message, e); } catch (CertificateValidationException e) { outboxStateRepository.decrementSendAttempts(messageId); lastFailure = e; - wasPermanentFailure = false; notifyUserIfCertificateProblem(account, e, false); handleSendFailure(account, localFolder, message, e); } catch (MessagingException e) { lastFailure = e; - wasPermanentFailure = e.isPermanentFailure(); - if (wasPermanentFailure) { + if (e.isPermanentFailure()) { String errorMessage = e.getMessage(); outboxStateRepository.setSendAttemptError(messageId, errorMessage); } else if (outboxState.getNumberOfSendAttempts() + 1 >= MAX_SEND_ATTEMPTS) { @@ -1618,24 +1614,19 @@ public class MessagingController { handleSendFailure(account, localFolder, message, e); } catch (Exception e) { lastFailure = e; - wasPermanentFailure = true; handleSendFailure(account, localFolder, message, e); } } catch (Exception e) { lastFailure = e; - wasPermanentFailure = false; + Timber.e(e, "Failed to fetch message for sending"); notifySynchronizeMailboxFailed(account, localFolder, e); } } if (lastFailure != null) { - if (wasPermanentFailure) { - notificationController.showSendFailedNotification(account, lastFailure); - } else { - notificationController.showSendFailedNotification(account, lastFailure); - } + notificationController.showSendFailedNotification(account, lastFailure); } } catch (Exception e) { Timber.v(e, "Failed to send pending messages"); -- GitLab From 570208b3fb96572df7f98f7e82146bb34c373e43 Mon Sep 17 00:00:00 2001 From: PatrickMis <24607131+PatrickMis@users.noreply.github.com> Date: Mon, 5 Sep 2022 02:57:42 +0200 Subject: [PATCH 82/85] Bump dependencies --- build.gradle | 8 ++++---- mail/protocols/webdav/build.gradle | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index b60b683cce..0ecfa39ec2 100644 --- a/build.gradle +++ b/build.gradle @@ -44,14 +44,14 @@ buildscript { 'commonsIo': '2.6', 'mime4j': '0.8.6', 'okhttp': '4.10.0', - 'minidns': '1.0.3', + 'minidns': '1.0.4', 'glide': '4.13.2', - 'jsoup': '1.15.2', + 'jsoup': '1.15.3', 'androidxTestRunner': '1.4.0', 'junit': '4.13.2', - 'robolectric': '4.8.1', - 'mockito': '4.6.1', + 'robolectric': '4.8.2', + 'mockito': '4.7.0', 'mockitoKotlin': '4.0.0', 'truth': '1.1.3', 'turbine': '0.9.0', diff --git a/mail/protocols/webdav/build.gradle b/mail/protocols/webdav/build.gradle index 49b4e08bd1..9572c044d1 100644 --- a/mail/protocols/webdav/build.gradle +++ b/mail/protocols/webdav/build.gradle @@ -14,11 +14,11 @@ dependencies { api project(":mail:common") implementation "commons-io:commons-io:${versions.commonsIo}" - compileOnly "org.apache.httpcomponents:httpclient:4.5.5" + compileOnly "org.apache.httpcomponents:httpclient:4.5.9" 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:4.5.5" + testImplementation "org.apache.httpcomponents:httpclient:4.5.9" } -- GitLab From 5cc022a9ee33a54b1b1445af144c90700b78bdb7 Mon Sep 17 00:00:00 2001 From: PatrickMis <24607131+PatrickMis@users.noreply.github.com> Date: Mon, 5 Sep 2022 13:40:21 +0200 Subject: [PATCH 83/85] Bump httpClient and move it to global 'versions' array --- build.gradle | 1 + mail/protocols/webdav/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 0ecfa39ec2..ce4e2491ae 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,7 @@ buildscript { 'minidns': '1.0.4', 'glide': '4.13.2', 'jsoup': '1.15.3', + 'httpClient': '4.5.13', 'androidxTestRunner': '1.4.0', 'junit': '4.13.2', diff --git a/mail/protocols/webdav/build.gradle b/mail/protocols/webdav/build.gradle index 9572c044d1..ee1e5fb27e 100644 --- a/mail/protocols/webdav/build.gradle +++ b/mail/protocols/webdav/build.gradle @@ -14,11 +14,11 @@ dependencies { api project(":mail:common") implementation "commons-io:commons-io:${versions.commonsIo}" - compileOnly "org.apache.httpcomponents:httpclient:4.5.9" + compileOnly "org.apache.httpcomponents:httpclient:${versions.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:4.5.9" + testImplementation "org.apache.httpcomponents:httpclient:${versions.httpClient}" } -- GitLab From 48431e2c40f1aef5d04559d60479161aeef191cf Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 8 Sep 2022 11:39:10 +0200 Subject: [PATCH 84/85] Ignore toolbar actions in message view while message is still loading --- .../main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index 6cf2845565..a7de139a0c 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -278,6 +278,8 @@ class MessageViewFragment : } override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (message == null) return false + when (item.itemId) { R.id.toggle_message_view_theme -> onToggleTheme() R.id.delete -> onDelete() -- GitLab From 6d904b6647a0dbd6d8ed85c4546307ee58b18522 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 8 Sep 2022 12:08:11 +0200 Subject: [PATCH 85/85] Version 6.303 --- app/k9mail/build.gradle | 4 ++-- app/ui/legacy/src/main/res/raw/changelog_master.xml | 6 ++++++ fastlane/metadata/android/en-US/changelogs/33003.txt | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/33003.txt diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 510c9b8784..1c93303199 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -48,8 +48,8 @@ android { applicationId "com.fsck.k9" testApplicationId "com.fsck.k9.tests" - versionCode 33002 - versionName '6.303-SNAPSHOT' + versionCode 33003 + versionName '6.303' // 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", diff --git a/app/ui/legacy/src/main/res/raw/changelog_master.xml b/app/ui/legacy/src/main/res/raw/changelog_master.xml index 4c8db3e7b1..02151b0f85 100644 --- a/app/ui/legacy/src/main/res/raw/changelog_master.xml +++ b/app/ui/legacy/src/main/res/raw/changelog_master.xml @@ -5,6 +5,12 @@ Locale-specific versions are kept in res/raw-/changelog.xml. --> + + Fixed a bug that lead to search being broken + Fixed error reporting for (old) send failures + Fixed "strip signatures on reply" + Fixed a crash when tapping a toolbar action in message view before loading the message has finished + Fixed moving to next/previous message when sorting the message list by read/unread or starred/unstarred Fixed a crash when a third-party app shared a file to K-9 Mail without granting access to it diff --git a/fastlane/metadata/android/en-US/changelogs/33003.txt b/fastlane/metadata/android/en-US/changelogs/33003.txt new file mode 100644 index 0000000000..75acd8d210 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/33003.txt @@ -0,0 +1,4 @@ +- Fixed a bug that lead to search being broken +- Fixed error reporting for (old) send failures +- Fixed "strip signatures on reply" +- Fixed a crash when tapping a toolbar action in message view before loading the message has finished -- GitLab