Loading app/ui/legacy/build.gradle +4 −0 Original line number Diff line number Diff line Loading @@ -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 Loading app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/EmailCache.kt 0 → 100644 +122 −0 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. */ 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<MessageListItem>? { if (isCacheShown) { return null } return runBlocking { fetchCachedMail() } } suspend fun saveLatestMails(mailList: List<MessageListItem>) { if (isCacheShown) return isCacheShown = true val cachedMailsWithLatest = getLatestMails(mailList) val listType = object : TypeToken<List<MessageListItem>>() {}.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<MessageReference>) { isCacheShown = false val cachedMessages = fetchCachedMail()?.toMutableList() cachedMessages?.let { messages.forEach { messageRef -> cachedMessages.removeIf { it.messageUid == messageRef.uid } saveLatestMails(cachedMessages) } } } private suspend fun fetchCachedMail(): List<MessageListItem>? { val listType = object : TypeToken<List<MessageListItem>>() {}.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<MessageListItem>): MutableList<MessageListItem> { var cachedMailsWithLatest = mutableListOf<MessageListItem>() 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<CharSequence?>() { @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() } } } app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt +8 −1 Original line number Diff line number Diff line 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 Loading @@ -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> { EmailCache(androidContext(), get()) } factory { MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get()) MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get(), emailCache = get()) } single { SortTypeToastProvider() } } app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading @@ -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() Loading Loading @@ -805,6 +809,9 @@ class MessageListFragment : } private fun onDeleteConfirmed(messages: List<MessageReference>) { lifecycleScope.launch { emailCache.deleteMail(messages) } if (showingThreadedList) { messagingController.deleteThreads(messages) } else { Loading app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveData.kt +8 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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<MessageListInfo>() { private val messageListChangedListener = MessageListChangedListener { Loading @@ -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) } } Loading Loading
app/ui/legacy/build.gradle +4 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/EmailCache.kt 0 → 100644 +122 −0 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. */ 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<MessageListItem>? { if (isCacheShown) { return null } return runBlocking { fetchCachedMail() } } suspend fun saveLatestMails(mailList: List<MessageListItem>) { if (isCacheShown) return isCacheShown = true val cachedMailsWithLatest = getLatestMails(mailList) val listType = object : TypeToken<List<MessageListItem>>() {}.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<MessageReference>) { isCacheShown = false val cachedMessages = fetchCachedMail()?.toMutableList() cachedMessages?.let { messages.forEach { messageRef -> cachedMessages.removeIf { it.messageUid == messageRef.uid } saveLatestMails(cachedMessages) } } } private suspend fun fetchCachedMail(): List<MessageListItem>? { val listType = object : TypeToken<List<MessageListItem>>() {}.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<MessageListItem>): MutableList<MessageListItem> { var cachedMailsWithLatest = mutableListOf<MessageListItem>() 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<CharSequence?>() { @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() } } }
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt +8 −1 Original line number Diff line number Diff line 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 Loading @@ -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> { EmailCache(androidContext(), get()) } factory { MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get()) MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get(), emailCache = get()) } single { SortTypeToastProvider() } }
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading @@ -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() Loading Loading @@ -805,6 +809,9 @@ class MessageListFragment : } private fun onDeleteConfirmed(messages: List<MessageReference>) { lifecycleScope.launch { emailCache.deleteMail(messages) } if (showingThreadedList) { messagingController.deleteThreads(messages) } else { Loading
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveData.kt +8 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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<MessageListInfo>() { private val messageListChangedListener = MessageListChangedListener { Loading @@ -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) } } Loading