Skip to content

Commit e7fd37a

Browse files
committed
fix: only cache album folder art on disk, everything else in memory
1 parent 4c27486 commit e7fd37a

2 files changed

Lines changed: 151 additions & 58 deletions

File tree

app/src/main/java/org/akanework/gramophone/logic/GramophoneApplication.kt

Lines changed: 4 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,9 @@ import androidx.core.net.toUri
3838
import coil3.ImageLoader
3939
import coil3.PlatformContext
4040
import coil3.SingletonImageLoader
41-
import coil3.Uri
42-
import coil3.asImage
43-
import coil3.decode.ContentMetadata
44-
import coil3.decode.DataSource
45-
import coil3.decode.ImageSource
46-
import coil3.fetch.Fetcher
47-
import coil3.fetch.ImageFetchResult
48-
import coil3.fetch.SourceFetchResult
49-
import coil3.key.Keyer
5041
import coil3.request.NullRequestDataException
51-
import coil3.size.pxOrElse
52-
import coil3.toCoilUri
5342
import coil3.util.Logger
5443
import coil3.disk.DiskCache
55-
import coil3.disk.directory
56-
import coil3.toAndroidUri
5744
import coil3.request.Options
5845
import kotlinx.coroutines.CoroutineScope
5946
import kotlinx.coroutines.Dispatchers
@@ -73,6 +60,7 @@ import org.akanework.gramophone.ui.LyricWidgetProvider
7360
import org.lsposed.hiddenapibypass.LSPass
7461
import org.nift4.gramophone.hificore.UacManager
7562
import org.akanework.gramophone.logic.utils.ArtResolver
63+
import org.akanework.gramophone.logic.utils.CoilArtPipeline
7664
import uk.akane.libphonograph.reader.FlowReader
7765
import uk.akane.libphonograph.utils.MiscUtils
7866
import java.io.File
@@ -265,33 +253,9 @@ class GramophoneApplication : Application(), SingletonImageLoader.Factory,
265253
.build()
266254
}
267255
.components {
268-
add(GramophoneArtKeyer())
269-
add(Fetcher.Factory { data, options, _ ->
270-
if (data !is Uri) return@Factory null
271-
if (data.scheme != "gramophoneSongCover" && data.scheme != "gramophoneAlbumCover") return@Factory null
272-
return@Factory Fetcher {
273-
val size = options.getArtworkBucketSize()
274-
val androidUri = data.toAndroidUri()
275-
276-
val candidates = ArtResolver.getResolutionList(androidUri, size)
277-
for (candidate in candidates) {
278-
val art = ArtResolver.openResourceStream(options.context, candidate, size)
279-
if (art != null) {
280-
return@Fetcher SourceFetchResult(
281-
source = ImageSource(
282-
source = art.stream.source().buffer(),
283-
fileSystem = options.fileSystem
284-
),
285-
mimeType = art.mimeType,
286-
dataSource = if (size <= 320 && (candidate is ArtResolver.ArtResource.SongMediaStore ||
287-
candidate is ArtResolver.ArtResource.AlbumMediaStore))
288-
DataSource.DISK else DataSource.MEMORY,
289-
)
290-
}
291-
}
292-
throw IOException("Unable to open '$data'.")
293-
}
294-
})
256+
add(CoilArtPipeline.ResolutionInterceptor())
257+
add(CoilArtPipeline.ArtResourceKeyer())
258+
add(CoilArtPipeline.ArtResourceFetcher.Factory())
295259
}
296260
.run {
297261
if (!BuildConfig.DEBUG) this else
@@ -353,21 +317,3 @@ class GramophoneApplication : Application(), SingletonImageLoader.Factory,
353317
}
354318
}
355319
}
356-
357-
private fun Options.getArtworkBucketSize(): Int {
358-
val requestWidth = size.width.pxOrElse { 0 }
359-
val requestHeight = size.height.pxOrElse { 0 }
360-
return if (requestWidth > 320 || requestHeight > 320) 1024 else 320
361-
}
362-
363-
class GramophoneArtKeyer : Keyer<Uri> {
364-
override fun key(data: Uri, options: Options): String? {
365-
if (data.scheme != "gramophoneSongCover" && data.scheme != "gramophoneAlbumCover") return null
366-
367-
val size = options.getArtworkBucketSize()
368-
val androidUri = data.toAndroidUri()
369-
370-
val candidates = ArtResolver.getResolutionList(androidUri, size)
371-
return candidates.firstOrNull()?.toCacheKey(size)
372-
}
373-
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package org.akanework.gramophone.logic.utils
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import coil3.ImageLoader
6+
import coil3.Uri
7+
import coil3.decode.DataSource
8+
import coil3.decode.ImageSource
9+
import coil3.fetch.FetchResult
10+
import coil3.fetch.Fetcher
11+
import coil3.fetch.SourceFetchResult
12+
import coil3.intercept.Interceptor
13+
import coil3.key.Keyer
14+
import coil3.request.ErrorResult
15+
import coil3.request.ImageResult
16+
import coil3.request.Options
17+
import coil3.request.SuccessResult
18+
import coil3.size.pxOrElse
19+
import coil3.toAndroidUri
20+
import coil3.toCoilUri
21+
import okio.buffer
22+
import okio.source
23+
import java.io.IOException
24+
25+
object CoilArtPipeline {
26+
27+
private const val TAG = "CoilArtPipeline"
28+
29+
fun Options.getArtworkBucketSize(): Int {
30+
val requestWidth = size.width.pxOrElse { 0 }
31+
val requestHeight = size.height.pxOrElse { 0 }
32+
return if (requestWidth > 320 || requestHeight > 320) 1024 else 320
33+
}
34+
35+
class ResolutionInterceptor : Interceptor {
36+
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
37+
val request = chain.request
38+
val data = request.data
39+
40+
if (data is ArtResolver.ArtResource) {
41+
Log.d(TAG, "Proceeding with ArtResource: $data")
42+
return chain.proceed()
43+
}
44+
45+
val coilUri = when (data) {
46+
is Uri -> data
47+
is android.net.Uri -> data.toCoilUri()
48+
else -> null
49+
}
50+
51+
if (coilUri != null && isSupportedUri(coilUri)) {
52+
val size = chain.size
53+
val androidUri = coilUri.toAndroidUri()
54+
// Use a stable bucket size for candidates to improve cache hits
55+
val bucketSize = if (size.width.pxOrElse { 0 } > 320 || size.height.pxOrElse { 0 } > 320) 1024 else 320
56+
val candidates = ArtResolver.getResolutionList(androidUri, bucketSize)
57+
58+
Log.d(TAG, "Intercepted artwork URI: $coilUri, bucketSize: $bucketSize, candidates: ${candidates.size}")
59+
60+
var lastError: Throwable? = null
61+
for (candidate in candidates) {
62+
val newRequest = request.newBuilder()
63+
.data(candidate)
64+
.build()
65+
66+
Log.d(TAG, " Trying candidate: $candidate")
67+
val result = chain.withRequest(newRequest).proceed()
68+
if (result is SuccessResult) {
69+
Log.d(TAG, " Successfully loaded candidate: $candidate")
70+
return result
71+
}
72+
if (result is ErrorResult) {
73+
Log.d(TAG, " Failed to load candidate: $candidate, error: ${result.throwable}")
74+
lastError = result.throwable
75+
}
76+
}
77+
Log.w(TAG, " All candidates failed for $coilUri")
78+
return ErrorResult(
79+
image = null,
80+
request = request,
81+
throwable = lastError ?: IOException("Unable to resolve $coilUri")
82+
)
83+
}
84+
return chain.proceed()
85+
}
86+
87+
private fun isSupportedUri(uri: Uri): Boolean {
88+
val scheme = uri.scheme
89+
val authority = uri.authority
90+
val supported = scheme == "gramophoneSongCover" ||
91+
scheme == "gramophoneAlbumCover" ||
92+
(scheme == "content" && (authority == ArtResolver.PROVIDER_AUTHORITY || (authority != null && authority.endsWith(".albumart"))))
93+
if (supported) {
94+
Log.v(TAG, "isSupportedUri: $uri -> true")
95+
}
96+
return supported
97+
}
98+
}
99+
100+
class ArtResourceKeyer : Keyer<ArtResolver.ArtResource> {
101+
override fun key(data: ArtResolver.ArtResource, options: Options): String {
102+
val key = data.toCacheKey(options.getArtworkBucketSize())
103+
Log.v(TAG, "Keyer for $data -> $key")
104+
return key
105+
}
106+
}
107+
108+
class ArtResourceFetcher(
109+
private val context: Context,
110+
private val candidate: ArtResolver.ArtResource,
111+
private val options: Options
112+
) : Fetcher {
113+
override suspend fun fetch(): FetchResult? {
114+
val size = options.getArtworkBucketSize()
115+
Log.v(TAG, "Fetcher.fetch for $candidate (size: $size)")
116+
val art = ArtResolver.openResourceStream(context, candidate, size) ?: run {
117+
Log.v(TAG, " Fetcher.fetch failed to open stream for $candidate")
118+
return null
119+
}
120+
121+
// Only AlbumFolder should be cached on disk.
122+
// DataSource.LOCAL signals to Coil that it should be written to the disk cache.
123+
// DataSource.DISK signals that it's already on disk (or shouldn't be cached there again).
124+
val shouldDiskCache = candidate is ArtResolver.ArtResource.AlbumFolder
125+
Log.v(TAG, " Fetcher.fetch success for $candidate, shouldDiskCache: $shouldDiskCache")
126+
127+
return SourceFetchResult(
128+
source = ImageSource(
129+
source = art.stream.source().buffer(),
130+
fileSystem = options.fileSystem
131+
),
132+
mimeType = art.mimeType,
133+
dataSource = if (shouldDiskCache) DataSource.MEMORY else DataSource.DISK
134+
)
135+
}
136+
137+
class Factory : Fetcher.Factory<ArtResolver.ArtResource> {
138+
override fun create(
139+
data: ArtResolver.ArtResource,
140+
options: Options,
141+
imageLoader: ImageLoader
142+
): Fetcher {
143+
return ArtResourceFetcher(options.context, data, options)
144+
}
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)