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

Commit 4a372f9d authored by Hasib Prince's avatar Hasib Prince
Browse files

Merge branch '938-cache_inbox' into 'main'

initial caching thorough datastore

See merge request !136
parents e9a68522 15577ff9
Loading
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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
+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()
        }
    }
}
+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

@@ -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() }
}
+7 −0
Original line number Diff line number Diff line
@@ -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<MessageReference>) {
        lifecycleScope.launch {
            emailCache.deleteMail(messages)
        }
        if (showingThreadedList) {
            messagingController.deleteThreads(messages)
        } else {
+8 −1
Original line number Diff line number Diff line
@@ -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<MessageListInfo>() {

    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)
        }
    }

Loading