Skip to content

Commit 0735b85

Browse files
banksionift4
authored andcommitted
Import albumart part of PR #890
[nift4: lightly edited]
1 parent f0aadad commit 0735b85

10 files changed

Lines changed: 704 additions & 125 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,12 @@
246246
android:exported="true"
247247
android:readPermission="android.permission.GLOBAL_SEARCH" />
248248

249+
<provider
250+
android:name=".logic.GramophoneAlbumArtProvider"
251+
android:authorities="${applicationId}.albumart"
252+
android:exported="true"
253+
android:grantUriPermissions="true" />
254+
249255
<provider
250256
android:name="androidx.core.content.FileProvider"
251257
android:authorities="${applicationId}.fileProvider"
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package org.akanework.gramophone.logic
2+
3+
import android.content.ContentProvider
4+
import android.content.ContentValues
5+
import android.database.Cursor
6+
import android.net.Uri
7+
import android.os.ParcelFileDescriptor
8+
import android.util.Log
9+
import kotlinx.coroutines.CoroutineScope
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.SupervisorJob
12+
import kotlinx.coroutines.launch
13+
import org.akanework.gramophone.logic.utils.ArtResolver
14+
import java.io.IOException
15+
16+
/**
17+
* ContentProvider that serves album artwork to external processes (e.g. Android Auto).
18+
*
19+
* External processes cannot resolve Gramophone's internal URI schemes
20+
* (`gramophoneSongCover://`, `gramophoneAlbumCover://`). This provider acts as a bridge,
21+
* using the shared [ArtResolver] and Coil's disk cache to resolve, cache and serve the artwork
22+
* over a standard `content://` URI.
23+
*
24+
* URI format: `content://org.akanework.gramophone.albumart/{type}/{id}/{encodedPath}`
25+
* where `type` is "song" or "album".
26+
*/
27+
class GramophoneAlbumArtProvider : ContentProvider() {
28+
29+
companion object {
30+
private const val TAG = "GramophoneArtProvider"
31+
}
32+
33+
private val providerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
34+
override fun onCreate() = true
35+
36+
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
37+
val context = context ?: return null
38+
val app = (context.applicationContext as? GramophoneApplication) ?: return null
39+
40+
try {
41+
val pipe = ParcelFileDescriptor.createPipe()
42+
val readSide = pipe[0]
43+
val writeSide = pipe[1]
44+
45+
providerScope.launch(Dispatchers.IO) {
46+
try {
47+
val size = 1024
48+
val candidates = ArtResolver.getResolutionList(uri, size)
49+
var found = false
50+
for (candidate in candidates) {
51+
val art = ArtResolver.openResourceStream(context, candidate, size)
52+
if (art != null) {
53+
art.stream.use { input ->
54+
ParcelFileDescriptor.AutoCloseOutputStream(writeSide).use { output ->
55+
input.copyTo(output)
56+
}
57+
}
58+
found = true
59+
break
60+
}
61+
}
62+
if (!found) {
63+
Log.w(TAG, "No artwork found for URI: $uri")
64+
try { writeSide.close() } catch (_: Exception) {}
65+
}
66+
} catch (e: Exception) {
67+
Log.e(TAG, "Error streaming artwork for URI: $uri", e)
68+
try { writeSide.close() } catch (_: Exception) {}
69+
}
70+
}
71+
return readSide
72+
} catch (e: IOException) {
73+
Log.e(TAG, "Failed to create pipe for URI: $uri", e)
74+
return null
75+
}
76+
}
77+
78+
override fun query(
79+
uri: Uri,
80+
projection: Array<out String>?,
81+
selection: String?,
82+
selectionArgs: Array<out String>?,
83+
sortOrder: String?
84+
): Cursor? = null
85+
86+
override fun getType(uri: Uri): String = "image/jpeg"
87+
88+
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
89+
90+
override fun delete(
91+
uri: Uri,
92+
selection: String?,
93+
selectionArgs: Array<out String>?
94+
): Int = 0
95+
96+
override fun update(
97+
uri: Uri,
98+
values: ContentValues?,
99+
selection: String?,
100+
selectionArgs: Array<out String>?
101+
): Int = 0
102+
}

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

Lines changed: 12 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,14 @@ package org.akanework.gramophone.logic
2020
import android.annotation.SuppressLint
2121
import android.app.Application
2222
import android.app.NotificationManager
23-
import android.content.ContentUris
2423
import android.content.Intent
2524
import android.content.SharedPreferences
26-
import android.graphics.Bitmap
2725
import android.os.Build
2826
import android.os.Debug
2927
import android.os.Environment
3028
import android.os.StrictMode
3129
import android.os.StrictMode.ThreadPolicy
3230
import android.os.StrictMode.VmPolicy
33-
import android.provider.MediaStore
34-
import android.util.Size
35-
import android.webkit.MimeTypeMap
3631
import androidx.appcompat.app.AppCompatDelegate
3732
import androidx.compose.runtime.Composer
3833
import androidx.compose.runtime.ExperimentalComposeRuntimeApi
@@ -44,19 +39,8 @@ import androidx.preference.PreferenceManager
4439
import coil3.ImageLoader
4540
import coil3.PlatformContext
4641
import coil3.SingletonImageLoader
47-
import coil3.Uri
48-
import coil3.asImage
49-
import coil3.decode.ContentMetadata
50-
import coil3.decode.DataSource
51-
import coil3.decode.ImageSource
52-
import coil3.fetch.Fetcher
53-
import coil3.fetch.ImageFetchResult
54-
import coil3.fetch.SourceFetchResult
42+
import coil3.disk.DiskCache
5543
import coil3.request.NullRequestDataException
56-
import coil3.request.allowHardware
57-
import coil3.size.pxOrElse
58-
import coil3.toBitmap
59-
import coil3.toCoilUri
6044
import coil3.util.Logger
6145
import kotlinx.coroutines.CoroutineScope
6246
import kotlinx.coroutines.Dispatchers
@@ -65,22 +49,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
6549
import kotlinx.coroutines.launch
6650
import kotlinx.coroutines.runBlocking
6751
import okio.Path.Companion.toOkioPath
68-
import okio.buffer
69-
import okio.source
7052
import org.akanework.gramophone.BuildConfig
7153
import org.akanework.gramophone.R
7254
import org.akanework.gramophone.logic.ui.BugHandlerActivity
73-
import org.akanework.gramophone.logic.utils.Flags
55+
import org.akanework.gramophone.logic.utils.CoilArtPipeline
7456
import org.akanework.gramophone.ui.LyricWidgetProvider
7557
import org.lsposed.hiddenapibypass.HiddenApiBypass
7658
import org.lsposed.hiddenapibypass.LSPass
7759
import org.nift4.gramophone.hificore.UacManager
78-
import org.nift4.mediastorecompat.MediaStoreCompat
79-
import org.nift4.mediastorecompat.ThumbnailUtilsCompat
80-
import uk.akane.libphonograph.Constants
8160
import uk.akane.libphonograph.reader.FlowReader
82-
import uk.akane.libphonograph.utils.MiscUtils
83-
import java.io.File
8461
import java.io.IOException
8562
import kotlin.system.exitProcess
8663

@@ -263,8 +240,7 @@ class GramophoneApplication : Application(), SingletonImageLoader.Factory,
263240
whiteListSetFlow,
264241
if (hasScopedStorageWithMediaTypes()) MutableStateFlow(null) else
265242
shouldUseEnhancedCoverReadingFlow!!,
266-
recentlyAddedFilterSecondFlow,
267-
"gramophoneAlbumCover"
243+
recentlyAddedFilterSecondFlow
268244
)
269245
// Set application theme when launching.
270246
when (prefs.getString("theme_mode", "0")) {
@@ -336,88 +312,16 @@ class GramophoneApplication : Application(), SingletonImageLoader.Factory,
336312

337313
override fun newImageLoader(context: PlatformContext): ImageLoader {
338314
return ImageLoader.Builder(context)
339-
.diskCache(null)
315+
.diskCache {
316+
DiskCache.Builder()
317+
.directory(context.cacheDir.resolve("image_cache").toOkioPath())
318+
.maxSizeBytes(50L * 1024 * 1024) // 50MB
319+
.build()
320+
}
340321
.components {
341-
add(Fetcher.Factory { data, options, _ ->
342-
if (data !is Uri) return@Factory null
343-
if (data.scheme != "gramophoneSongCover") return@Factory null
344-
return@Factory Fetcher {
345-
val file = File(data.path!!)
346-
val uri = ContentUris.appendId(
347-
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon(),
348-
data.authority!!.toLong()
349-
).appendPath(MEDIA_ALBUM_ART).build()
350-
val bmp = if (options.size.width.pxOrElse { 0 } > 300
351-
&& options.size.height.pxOrElse { 0 } > 300) try {
352-
ThumbnailUtilsCompat.createAudioThumbnail(file, options.size.let {
353-
Size(
354-
it.width.pxOrElse { throw IllegalArgumentException("missing required size") },
355-
it.height.pxOrElse { throw IllegalArgumentException("missing required size") })
356-
}, null)
357-
} catch (e: IOException) {
358-
if (e.message != "No embedded album art found" &&
359-
e.message != "No thumbnails in Downloads directories" &&
360-
e.message != "No thumbnails in top-level directories" &&
361-
e.message != "No album art found"
362-
)
363-
throw e
364-
null
365-
} else null
366-
if (bmp != null) {
367-
// This would crash while drawing if we don't catch it here
368-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
369-
bmp.config == Bitmap.Config.HARDWARE && !options.allowHardware)
370-
throw IllegalStateException("Got hardware bitmap unexpectedly")
371-
ImageFetchResult(
372-
bmp.asImage(), true, DataSource.DISK
373-
)
374-
} else {
375-
if (uri == null) return@Fetcher null
376-
val stream = contentResolver.openAssetFileDescriptor(uri, "r")
377-
checkNotNull(stream) { "Unable to open '$uri'." }
378-
SourceFetchResult(
379-
source = ImageSource(
380-
source = stream.createInputStream().source().buffer(),
381-
fileSystem = options.fileSystem,
382-
metadata = ContentMetadata(uri.toCoilUri(), stream),
383-
),
384-
mimeType = contentResolver.getType(uri),
385-
dataSource = DataSource.DISK,
386-
)
387-
}
388-
}
389-
})
390-
add(Fetcher.Factory { data, options, _ ->
391-
if (data !is Uri) return@Factory null
392-
if (data.scheme != "gramophoneAlbumCover") return@Factory null
393-
return@Factory Fetcher {
394-
val cover = MiscUtils.findBestCover(File(data.path!!))
395-
if (cover == null) {
396-
val uri =
397-
ContentUris.withAppendedId(
398-
Constants.baseAlbumCoverUri,
399-
data.authority!!.toLong()
400-
)
401-
val contentResolver = options.context.contentResolver
402-
val afd = contentResolver.openAssetFileDescriptor(uri, "r")
403-
checkNotNull(afd) { "Unable to open '$uri'." }
404-
return@Fetcher SourceFetchResult(
405-
source = ImageSource(
406-
source = afd.createInputStream().source().buffer(),
407-
fileSystem = options.fileSystem,
408-
metadata = ContentMetadata(data, afd),
409-
),
410-
mimeType = contentResolver.getType(uri),
411-
dataSource = DataSource.DISK,
412-
)
413-
}
414-
return@Fetcher SourceFetchResult(
415-
ImageSource(cover.toOkioPath(), options.fileSystem, null, null, null),
416-
MimeTypeMap.getSingleton().getMimeTypeFromExtension(cover.extension),
417-
DataSource.DISK
418-
)
419-
}
420-
})
322+
add(CoilArtPipeline.ResolutionInterceptor())
323+
add(CoilArtPipeline.ArtResourceKeyer())
324+
add(CoilArtPipeline.ArtResourceFetcher.Factory())
421325
}
422326
.run {
423327
if (!BuildConfig.DEBUG) this else

0 commit comments

Comments
 (0)