Attachments: implements proper caching
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user