Private
Public Access
1
0

Attachments: implements proper caching

This commit is contained in:
2024-04-09 00:11:53 -07:00
parent 20e33f70a8
commit d474ce1c10
5 changed files with 67 additions and 28 deletions

View File

@@ -1,16 +1,16 @@
package net.buzzert.kordophonedroid package net.buzzert.kordophonedroid
import android.content.Context
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import net.buzzert.kordophone.backend.db.CachedChatDatabase import net.buzzert.kordophone.backend.db.CachedChatDatabase
import net.buzzert.kordophone.backend.server.APIClientFactory import net.buzzert.kordophone.backend.server.APIClientFactory
import net.buzzert.kordophone.backend.server.Authentication
import net.buzzert.kordophone.backend.server.ChatRepository import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophone.backend.server.RetrofitAPIClient import net.buzzert.kordophonedroid.ui.attachments.AttachmentImageLoader
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
import java.net.URL
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -28,4 +28,14 @@ object AppModule {
val database = CachedChatDatabase.liveDatabase() val database = CachedChatDatabase.liveDatabase()
return ChatRepository(client, database) return ChatRepository(client, database)
} }
@Singleton
@Provides
fun provideAttachmentFactory(
chatRepository: ChatRepository,
@ApplicationContext context: Context
): AttachmentImageLoader
{
return AttachmentImageLoader(chatRepository, context)
}
} }

View File

@@ -6,29 +6,25 @@ import androidx.lifecycle.ViewModel
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import coil.annotation.ExperimentalCoilApi
import coil.decode.DataSource import coil.decode.DataSource
import coil.decode.ImageSource import coil.decode.ImageSource
import coil.disk.DiskCache import coil.disk.DiskCache
import coil.fetch.FetchResult import coil.fetch.FetchResult
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.fetch.SourceResult import coil.fetch.SourceResult
import coil.memory.MemoryCache
import coil.request.Options import coil.request.Options
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import net.buzzert.kordophone.backend.server.ChatRepository import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophonedroid.ui.messagelist.MVM_LOG
import javax.inject.Inject
const val AVM_LOG: String = "AttachmentViewModel" const val AVM_LOG: String = "AttachmentImageLoader"
data class AttachmentFetchData( data class AttachmentFetchData(
val guid: String, val guid: String,
val preview: Boolean = false val preview: Boolean = false
) )
@HiltViewModel class AttachmentImageLoader(
class AttachmentViewModel @Inject constructor(
private val repository: ChatRepository, private val repository: ChatRepository,
@ApplicationContext val application: Context, @ApplicationContext val application: Context,
) : ViewModel(), ImageLoaderFactory, Fetcher.Factory<AttachmentFetchData> ) : ViewModel(), ImageLoaderFactory, Fetcher.Factory<AttachmentFetchData>
@@ -41,17 +37,6 @@ class AttachmentViewModel @Inject constructor(
override fun newImageLoader(): ImageLoader { override fun newImageLoader(): ImageLoader {
val factory = this val factory = this
return ImageLoader.Builder(application) return ImageLoader.Builder(application)
.memoryCache {
MemoryCache.Builder(application)
.maxSizePercent(0.25)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(application.cacheDir.resolve("attachments"))
.maxSizePercent(0.02)
.build()
}
.components { .components {
// Adds the FetcherFactory // Adds the FetcherFactory
add(factory) add(factory)
@@ -73,13 +58,59 @@ private class AttachmentFetcher(
val context: Context, val context: Context,
val data: AttachmentFetchData val data: AttachmentFetchData
): Fetcher { ): Fetcher {
val cache = DiskCache.Builder()
.directory(context.cacheDir.resolve("attachments"))
.maxSizePercent(0.02)
.build()
val cacheKey: String get() { return data.guid + if (data.preview) "_preview" else "" }
@OptIn(ExperimentalCoilApi::class)
override suspend fun fetch(): FetchResult { override suspend fun fetch(): FetchResult {
// Try loading from cache
var snapshot = cache.openSnapshot(cacheKey)
if (snapshot != null) {
Log.d(AVM_LOG, "Found attachment ${data.guid} in disk cache")
return SourceResult(
source = snapshot.toImageSource(),
dataSource = DataSource.DISK,
mimeType = null,
)
}
Log.d(AVM_LOG, "Loading attachment ${data.guid} from network") Log.d(AVM_LOG, "Loading attachment ${data.guid} from network")
val source = repository.fetchAttachmentDataSource(data.guid, data.preview) val source = repository.fetchAttachmentDataSource(data.guid, data.preview)
// Save to cache
val editor = cache.openEditor(cacheKey)
if (editor != null) {
Log.d(AVM_LOG, "Writing attachment ${data.guid} to disk cache")
cache.fileSystem.write(editor.data) {
source.readAll(this)
}
snapshot = editor.commitAndOpenSnapshot()
if (snapshot != null) {
return SourceResult(
source = snapshot.toImageSource(),
dataSource = DataSource.NETWORK,
mimeType = null
)
}
}
// We'll go down this path if for some reason we couldn't save to cache.
return SourceResult( return SourceResult(
source = ImageSource(source, context), source = ImageSource(source, context),
dataSource = DataSource.NETWORK, dataSource = DataSource.NETWORK,
mimeType = null, mimeType = null,
) )
} }
@OptIn(ExperimentalCoilApi::class)
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
val fileSystem = cache.fileSystem
return ImageSource(data, fileSystem, cacheKey, this)
}
} }

View File

@@ -25,10 +25,7 @@ import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable import net.engawapg.lib.zoomable.zoomable
@Composable @Composable
fun AttachmentViewer( fun AttachmentViewer(attachmentGuid: String) {
attachmentGuid: String,
attachmentViewModel: AttachmentViewModel = hiltViewModel()
) {
var topBarVisible = remember { mutableStateOf(true) } var topBarVisible = remember { mutableStateOf(true) }
val navController = LocalNavController.current val navController = LocalNavController.current
Scaffold(topBar = { Scaffold(topBar = {

View File

@@ -55,7 +55,6 @@ import net.buzzert.kordophone.backend.model.GUID
import net.buzzert.kordophonedroid.ui.Destination import net.buzzert.kordophonedroid.ui.Destination
import net.buzzert.kordophonedroid.ui.LocalNavController import net.buzzert.kordophonedroid.ui.LocalNavController
import net.buzzert.kordophonedroid.ui.attachments.AttachmentFetchData import net.buzzert.kordophonedroid.ui.attachments.AttachmentFetchData
import net.buzzert.kordophonedroid.ui.attachments.AttachmentViewModel
import net.buzzert.kordophonedroid.ui.shared.LINK_ANNOTATION_TAG import net.buzzert.kordophonedroid.ui.shared.LINK_ANNOTATION_TAG
import net.buzzert.kordophonedroid.ui.shared.linkify import net.buzzert.kordophonedroid.ui.shared.linkify
import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar
@@ -86,7 +85,6 @@ sealed class MessageListItem: MessageMetadataProvider {
fun MessageListScreen( fun MessageListScreen(
conversationGUID: GUID, conversationGUID: GUID,
viewModel: MessageListViewModel = hiltViewModel(), viewModel: MessageListViewModel = hiltViewModel(),
attachmentViewModel: AttachmentViewModel = hiltViewModel() // unused, but initialized for Coil
) { ) {
viewModel.conversationGUID = conversationGUID viewModel.conversationGUID = conversationGUID

View File

@@ -3,6 +3,7 @@ package net.buzzert.kordophonedroid.ui.messagelist
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@@ -18,13 +19,15 @@ import net.buzzert.kordophone.backend.model.Message
import net.buzzert.kordophone.backend.model.OutgoingMessage import net.buzzert.kordophone.backend.model.OutgoingMessage
import net.buzzert.kordophone.backend.model.UploadingAttachmentMetadata import net.buzzert.kordophone.backend.model.UploadingAttachmentMetadata
import net.buzzert.kordophone.backend.server.ChatRepository import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophonedroid.ui.attachments.AttachmentImageLoader
import javax.inject.Inject import javax.inject.Inject
const val MVM_LOG: String = "MessageListViewModel" const val MVM_LOG: String = "MessageListViewModel"
@HiltViewModel @HiltViewModel
class MessageListViewModel @Inject constructor( class MessageListViewModel @Inject constructor(
private val repository: ChatRepository private val repository: ChatRepository,
private val imageLoader: AttachmentImageLoader
) : ViewModel() ) : ViewModel()
{ {
var conversationGUID: GUID? = null var conversationGUID: GUID? = null