diff --git a/app/ui/legacy/build.gradle b/app/ui/legacy/build.gradle index 2e4f957a751dc694f73182a050c3db2b26f0ea6f..36ef6c0a2ea05c65004a57786fd245d9d2d807dd 100644 --- a/app/ui/legacy/build.gradle +++ b/app/ui/legacy/build.gradle @@ -42,6 +42,10 @@ dependencies { implementation libs.fastadapter.extensions.drag implementation libs.fastadapter.extensions.utils implementation libs.circleimageview + implementation libs.moshi + implementation libs.moshi.kotlin + implementation libs.gson + implementation libs.androidx.datastore api libs.appauth implementation libs.commons.io diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/EmailCache.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/EmailCache.kt new file mode 100644 index 0000000000000000000000000000000000000000..f1ff13198cf139101d592e32d523d107db10d0db --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/EmailCache.kt @@ -0,0 +1,122 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.fsck.k9.ui.messagelist + +import android.content.Context +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.fsck.k9.Account +import com.fsck.k9.controller.MessageReference +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import java.io.IOException +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking +import timber.log.Timber + +class EmailCache constructor(private val context: Context, private val gson: Gson) { + companion object { + private const val MAX_CACHE_SIZE = 20 + private const val preferenceDataStoreName = "emailCache" + private val Context.emailCacheDataStore by preferencesDataStore(preferenceDataStoreName) + } + + private val MAIL_LIST_KEY = stringPreferencesKey("mail_list") + private var isCacheShown = false + + suspend fun getCachedMails(): List? { + if (isCacheShown) { + return null + } + + return runBlocking { + fetchCachedMail() + } + } + + suspend fun saveLatestMails(mailList: List) { + if (isCacheShown) return + + isCacheShown = true + val cachedMailsWithLatest = getLatestMails(mailList) + + val listType = object : TypeToken>() {}.type + val mailListJson = gson.toJson(cachedMailsWithLatest, listType) + context.emailCacheDataStore.edit { + it[MAIL_LIST_KEY] = mailListJson + } + Timber.d("Saved latest mails in the cache") + } + + suspend fun deleteMail(messages: List) { + isCacheShown = false + val cachedMessages = fetchCachedMail()?.toMutableList() + cachedMessages?.let { + messages.forEach { messageRef -> + cachedMessages.removeIf { it.messageUid == messageRef.uid } + saveLatestMails(cachedMessages) + } + } + } + + private suspend fun fetchCachedMail(): List? { + val listType = object : TypeToken>() {}.type + val mailListJson = context.emailCacheDataStore.data.map { it[MAIL_LIST_KEY] }.firstOrNull() + Timber.d("Cached email data: $mailListJson") + return gson.fromJson(mailListJson, listType) + } + + private suspend fun getLatestMails(mailList: List): MutableList { + var cachedMailsWithLatest = mutableListOf() + cachedMailsWithLatest.addAll(mailList) + fetchCachedMail()?.let { + cachedMailsWithLatest.addAll(it) + } + + cachedMailsWithLatest.sortedByDescending { it.messageDate } + val lastIndex = if (cachedMailsWithLatest.size < MAX_CACHE_SIZE) cachedMailsWithLatest.size else MAX_CACHE_SIZE + cachedMailsWithLatest = cachedMailsWithLatest.subList(0, lastIndex) + return cachedMailsWithLatest + } +} + +internal class CharSequenceTypeAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun write(writer: JsonWriter, value: CharSequence?) { + if (value == null) { + writer.nullValue() + } else { + writer.value(value.toString()) + } + } + + @Throws(IOException::class) + override fun read(reader: JsonReader): CharSequence? { + return if (reader.peek() === JsonToken.NULL) { + reader.skipValue() + null + } else { + reader.nextString() + } + } +} 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 487fcea1c558a811a2624ec127971fd8e2286f7e..95ffa93e130313d661c279be2903f96fc7437b28 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 @@ -1,5 +1,9 @@ package com.fsck.k9.ui.messagelist +import com.google.gson.GsonBuilder +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -14,8 +18,11 @@ val messageListUiModule = module { messageHelper = get() ) } + single { Moshi.Builder().add(KotlinJsonAdapterFactory()) .build() } + single { GsonBuilder().registerTypeAdapter(CharSequence::class.java, CharSequenceTypeAdapter()).create() } + single { EmailCache(androidContext(), get()) } factory { - MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get()) + MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get(), emailCache = get()) } single { SortTypeToastProvider() } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt index 9f5261fdf161239d7faea05c43f273afe4d9b0c2..1979715e582efb65f3fd532518fcb1562dfb19b4 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt @@ -22,6 +22,7 @@ import androidx.core.view.setPadding import androidx.fragment.app.Fragment import androidx.recyclerview.widget.DefaultItemAnimator import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.OnScrollListener import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE @@ -64,6 +65,8 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback import com.google.android.material.snackbar.Snackbar import java.util.concurrent.Future +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch import net.jcip.annotations.GuardedBy import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel @@ -88,6 +91,7 @@ class MessageListFragment : private val messagingController: MessagingController by inject() private val preferences: Preferences by inject() private val clock: Clock by inject() + private val emailCache: EmailCache by inject() private val handler = MessageListHandler(this) private val activityListener = MessageListActivityListener() @@ -805,6 +809,9 @@ class MessageListFragment : } private fun onDeleteConfirmed(messages: List) { + lifecycleScope.launch { + emailCache.deleteMail(messages) + } if (showingThreadedList) { messagingController.deleteThreads(messages) } else { 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 bf757521d24ff89ef52ac88bf86691e44c068281..e8e517a5bed00ac88e7fd288ea7bcbe8f5727f44 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 @@ -5,6 +5,7 @@ import com.fsck.k9.Preferences import com.fsck.k9.mailstore.MessageListChangedListener import com.fsck.k9.mailstore.MessageListRepository import com.fsck.k9.search.getAccountUuids +import com.fsck.k9.search.getAccounts import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -15,7 +16,8 @@ class MessageListLiveData( private val preferences: Preferences, private val messageListRepository: MessageListRepository, private val coroutineScope: CoroutineScope, - val config: MessageListConfig + val config: MessageListConfig, + val emailCache: EmailCache ) : LiveData() { private val messageListChangedListener = MessageListChangedListener { @@ -24,10 +26,15 @@ class MessageListLiveData( private fun loadMessageListAsync() { coroutineScope.launch(Dispatchers.Main) { + emailCache.getCachedMails()?.let { + value = MessageListInfo(it, true) + } + val messageList = withContext(Dispatchers.IO) { messageListLoader.getMessageList(config) } value = messageList + emailCache.saveLatestMails(messageList.messageListItems) } } 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 af995910f10bc08f7b2885f774e2102603fe7ce7..236f679c117c690e0c2b67d31f6eaa17b9b60128 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 @@ -7,9 +7,10 @@ import kotlinx.coroutines.CoroutineScope class MessageListLiveDataFactory( private val messageListLoader: MessageListLoader, private val preferences: Preferences, - private val messageListRepository: MessageListRepository + private val messageListRepository: MessageListRepository, + private val emailCache: EmailCache ) { fun create(coroutineScope: CoroutineScope, config: MessageListConfig): MessageListLiveData { - return MessageListLiveData(messageListLoader, preferences, messageListRepository, coroutineScope, config) + return MessageListLiveData(messageListLoader, preferences, messageListRepository, coroutineScope, config, emailCache) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 123d4e8720dc8be00e68ba8fbb327ebe0abffdf6..c2c24ab364853baf0fc65f3ebdb1e8bead285ebb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,7 @@ okhttp = "4.10.0" glide = "4.14.2" moshi = "1.14.0" mockito = "5.0.0" +datastore = "1.0.0" [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } @@ -70,6 +71,7 @@ androidx-preference = { module = "androidx.preference:preference", version.ref = androidx-swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" androidx-test-core = "androidx.test:core:1.5.0" android-material = "com.google.android.material:material:1.7.0" +androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } fastadapter = { module = "com.mikepenz:fastadapter", version.ref = "fastAdapter" } fastadapter-extensions-drag = { module = "com.mikepenz:fastadapter-extensions-drag", version.ref = "fastAdapter" } fastadapter-extensions-utils = { module = "com.mikepenz:fastadapter-extensions-utils", version.ref = "fastAdapter" } @@ -79,7 +81,9 @@ preferencex-datetimepicker = { module = "com.takisoft.preferencex:preferencex-da preferencex-colorpicker = { module = "com.takisoft.preferencex:preferencex-colorpicker", version.ref = "preferencesFix" } okio = "com.squareup.okio:okio:3.3.0" moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } +moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } +gson = "com.google.code.gson:gson:2.8.9" timber = "com.jakewharton.timber:timber:5.0.1" koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinCore" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" }