Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8183f989 authored by Mohammed Althaf T's avatar Mohammed Althaf T 😊
Browse files

mail: add EmailCache

parent 5531fd2c
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -46,4 +46,7 @@ object AccountManagerConstants {
    val YAHOO_DOMAIN_LIST = listOf(
        ".yahoo.com",
    )

    const val MAX_CACHE_SIZE = 20
    const val PREFERENCE_DATASTORE_NAME = "emailCache"
}
+4 −0
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ circleImageView = "3.1.0"
ckchangelog = "2.0.0-beta02"
clikt = "5.0.2"
commonsIo = "2.16.1"
datastorePreferences = "1.1.1"
dependencyCheckPlugin = "0.51.0"
dependencyGuardPlugin = "0.5.0"
detektPlugin = "1.23.5"
@@ -57,6 +58,7 @@ fastAdapter = "5.7.0"
forkhandlesBom = "2.20.0.0"
glide = "4.16.0"
gradle = "8.11.1"
gson = "2.11.0"
http4kBom = "5.35.2.0"
icu4j = "72.1"
javaDiffUtils = "4.12"
@@ -149,6 +151,7 @@ androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorl
androidx-core = { module = "androidx.core:core", version.ref = "androidxCore" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxCoreSplashscreen" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-fragment = { module = "androidx.fragment:fragment", version.ref = "androidxFragment" }
androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version.ref = "androidxFragment" }
androidx-fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "androidxFragment" }
@@ -193,6 +196,7 @@ forkhandles-bom = { module = "dev.forkhandles:forkhandles-bom", version.ref = "f
forkhandles-fabrikate4k = { module = "dev.forkhandles:fabrikate4k" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
http4k-bom = { module = "org.http4k:http4k-bom", version.ref = "http4kBom" }
http4k-core = { module = "org.http4k:http4k-core" }
http4k-client-okhttp = { module = "org.http4k:http4k-client-okhttp" }
+3 −0
Original line number Diff line number Diff line
@@ -10,6 +10,9 @@ dependencies {
    api(projects.core.ui.legacy.designsystem)
    implementation(projects.feature.account.accountmanager)

    implementation(libs.gson)
    implementation(libs.androidx.datastore.preferences)

    implementation(projects.legacy.core)
    implementation(projects.mail.common)
    implementation(projects.uiUtils.toolbarBottomSheet)
+28 −0
Original line number Diff line number Diff line
package com.fsck.k9.activity.accountmanager

import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import java.io.IOException

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()
        }
    }
}
+105 −0
Original line number Diff line number Diff line
/*
 * Copyright MURENA SAS 2024
 * 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.activity.accountmanager

import android.content.Context
import android.util.Log
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import app.k9mail.feature.account.accountmanager.AccountManagerConstants
import app.k9mail.legacy.message.controller.MessageReference
import com.fsck.k9.Preferences
import com.fsck.k9.logging.Timber
import com.fsck.k9.ui.messagelist.MessageListItem
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking

class EmailCache(private val context: Context, private val preferences: Preferences) {
    private val tag = "EmailCache"

    private val Context.emailCacheDataStore by preferencesDataStore(
        AccountManagerConstants.PREFERENCE_DATASTORE_NAME)
    private val mailListKey = stringPreferencesKey("e_mail_list")
    private var isCacheShown = false

    private val gson: Gson = GsonBuilder()
        .registerTypeAdapter(CharSequence::class.java, CharSequenceTypeAdapter())
        .create()

    private val listType = object : TypeToken<List<EMessageListItem>>() {}.type

    fun getCachedMails(): List<MessageListItem>? {
        if (isCacheShown) return null
        return runBlocking { fetchCachedMails() }
    }

    suspend fun saveLatestMails(mailList: List<MessageListItem>) {
        if (isCacheShown) return
        isCacheShown = true

        val updatedMails = mergeAndLimitCache(mailList)
        val serializedData = gson.toJson(updatedMails, listType)

        context.emailCacheDataStore.edit { preferences ->
            preferences[mailListKey] = serializedData
        }

        Timber.d("$tag: Saved latest mails in the cache")
    }

    suspend fun deleteMail(messagesToDelete: List<MessageReference>) {
        isCacheShown = false

        val updatedMails = fetchCachedMails().toMutableList().apply {
            removeAll { cachedMessage ->
                messagesToDelete.any { it.uid == cachedMessage.messageUid }
            }
        }

        saveLatestMails(updatedMails)
        Timber.d("$tag: Updated cache after deleting mails: $updatedMails")
    }

    private suspend fun fetchCachedMails(): List<MessageListItem> {
        val serializedData = context.emailCacheDataStore.data
            .map { it[mailListKey] }
            .firstOrNull()

        Timber.d("$tag: Cached email data: $serializedData")
        if (serializedData.isNullOrEmpty()) return emptyList()

        val cachedItems: List<EMessageListItem> = gson.fromJson(serializedData, listType)
        val accounts = preferences.getAccounts()

        return convertToMessageListItemList(cachedItems, accounts)
    }

    private suspend fun mergeAndLimitCache(
        newMails: List<MessageListItem>
    ): List<EMessageListItem> {
        val cachedMails = fetchCachedMails()
        val combinedMails = (newMails + cachedMails).distinctBy { it.messageUid }
            .sortedByDescending { it.messageDate }

        val limitedMails = combinedMails.take(AccountManagerConstants.MAX_CACHE_SIZE)
        return convertToEMessageListItemList(limitedMails)
    }
}
Loading