From d474ce1c10e458cc5cd9fe858360e4412222d8b7 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 9 Apr 2024 00:11:53 -0700 Subject: [PATCH] Attachments: implements proper caching --- .../net/buzzert/kordophonedroid/AppModule.kt | 16 ++++- .../ui/attachments/AttachmentViewModel.kt | 67 ++++++++++++++----- .../ui/attachments/AttachmentViewer.kt | 5 +- .../ui/messagelist/MessageListScreen.kt | 2 - .../ui/messagelist/MessageListViewModel.kt | 5 +- 5 files changed, 67 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/net/buzzert/kordophonedroid/AppModule.kt b/app/src/main/java/net/buzzert/kordophonedroid/AppModule.kt index db19a1c..9bbc955 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/AppModule.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/AppModule.kt @@ -1,16 +1,16 @@ package net.buzzert.kordophonedroid +import android.content.Context import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import net.buzzert.kordophone.backend.db.CachedChatDatabase 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.RetrofitAPIClient +import net.buzzert.kordophonedroid.ui.attachments.AttachmentImageLoader import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository -import java.net.URL import javax.inject.Singleton @Module @@ -28,4 +28,14 @@ object AppModule { val database = CachedChatDatabase.liveDatabase() return ChatRepository(client, database) } + + @Singleton + @Provides + fun provideAttachmentFactory( + chatRepository: ChatRepository, + @ApplicationContext context: Context + ): AttachmentImageLoader + { + return AttachmentImageLoader(chatRepository, context) + } } \ No newline at end of file diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewModel.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewModel.kt index 0ba86ac..8249693 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewModel.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewModel.kt @@ -6,29 +6,25 @@ import androidx.lifecycle.ViewModel import coil.Coil import coil.ImageLoader import coil.ImageLoaderFactory +import coil.annotation.ExperimentalCoilApi import coil.decode.DataSource import coil.decode.ImageSource import coil.disk.DiskCache import coil.fetch.FetchResult import coil.fetch.Fetcher import coil.fetch.SourceResult -import coil.memory.MemoryCache import coil.request.Options -import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext 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( val guid: String, val preview: Boolean = false ) -@HiltViewModel -class AttachmentViewModel @Inject constructor( +class AttachmentImageLoader( private val repository: ChatRepository, @ApplicationContext val application: Context, ) : ViewModel(), ImageLoaderFactory, Fetcher.Factory @@ -41,17 +37,6 @@ class AttachmentViewModel @Inject constructor( override fun newImageLoader(): ImageLoader { val factory = this 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 { // Adds the FetcherFactory add(factory) @@ -73,13 +58,59 @@ private class AttachmentFetcher( val context: Context, val data: AttachmentFetchData ): 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 { + // 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") 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( source = ImageSource(source, context), dataSource = DataSource.NETWORK, mimeType = null, ) } + + @OptIn(ExperimentalCoilApi::class) + private fun DiskCache.Snapshot.toImageSource(): ImageSource { + val fileSystem = cache.fileSystem + return ImageSource(data, fileSystem, cacheKey, this) + } } \ No newline at end of file diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewer.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewer.kt index b670103..698a380 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewer.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewer.kt @@ -25,10 +25,7 @@ import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable @Composable -fun AttachmentViewer( - attachmentGuid: String, - attachmentViewModel: AttachmentViewModel = hiltViewModel() -) { +fun AttachmentViewer(attachmentGuid: String) { var topBarVisible = remember { mutableStateOf(true) } val navController = LocalNavController.current Scaffold(topBar = { diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt index 354f809..b79e173 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt @@ -55,7 +55,6 @@ import net.buzzert.kordophone.backend.model.GUID import net.buzzert.kordophonedroid.ui.Destination import net.buzzert.kordophonedroid.ui.LocalNavController 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.linkify import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar @@ -86,7 +85,6 @@ sealed class MessageListItem: MessageMetadataProvider { fun MessageListScreen( conversationGUID: GUID, viewModel: MessageListViewModel = hiltViewModel(), - attachmentViewModel: AttachmentViewModel = hiltViewModel() // unused, but initialized for Coil ) { viewModel.conversationGUID = conversationGUID diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt index 7c62b8d..190729c 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt @@ -3,6 +3,7 @@ package net.buzzert.kordophonedroid.ui.messagelist import android.content.Context import android.net.Uri import android.webkit.MimeTypeMap +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.UploadingAttachmentMetadata import net.buzzert.kordophone.backend.server.ChatRepository +import net.buzzert.kordophonedroid.ui.attachments.AttachmentImageLoader import javax.inject.Inject const val MVM_LOG: String = "MessageListViewModel" @HiltViewModel class MessageListViewModel @Inject constructor( - private val repository: ChatRepository + private val repository: ChatRepository, + private val imageLoader: AttachmentImageLoader ) : ViewModel() { var conversationGUID: GUID? = null