From 7906867a96255ec2d22a12178d19e44eb25723d2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 26 Feb 2025 16:04:40 -0700 Subject: [PATCH] image: implement compat covers backport For cover.jpg users --- .../org/oxycblt/auxio/image/CoverProvider.kt | 16 +++-- .../auxio/image/covers/CompatCovers.kt | 72 +++++++++++++++++++ .../oxycblt/auxio/image/covers/NullCovers.kt | 3 +- .../auxio/image/covers/SettingCovers.kt | 17 ++++- .../auxio/image/covers/SiloedCovers.kt | 8 +-- .../oxycblt/auxio/music/MusicRepository.kt | 2 +- .../java/org/oxycblt/musikr/cover/Covers.kt | 8 +-- 7 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/image/covers/CompatCovers.kt diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt index 1c2a45820..8515f3b26 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt @@ -25,13 +25,19 @@ import android.content.UriMatcher import android.database.Cursor import android.net.Uri import android.os.ParcelFileDescriptor +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.runBlocking import org.oxycblt.auxio.BuildConfig +import org.oxycblt.auxio.image.covers.SettingCovers import org.oxycblt.auxio.image.covers.SiloedCoverId import org.oxycblt.auxio.image.covers.SiloedCovers import org.oxycblt.musikr.cover.CoverResult +import javax.inject.Inject -class CoverProvider : ContentProvider() { +@AndroidEntryPoint +class CoverProvider @Inject constructor( + private val settingCovers: SettingCovers +) : ContentProvider() { override fun onCreate(): Boolean = true override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { @@ -39,12 +45,10 @@ class CoverProvider : ContentProvider() { return null } val id = uri.lastPathSegment ?: return null - val coverId = SiloedCoverId.parse(id) ?: return null return runBlocking { - val siloedCovers = SiloedCovers.from(requireNotNull(context), coverId.silo) - when (val res = siloedCovers.obtain(id)) { - is CoverResult.Hit -> res.cover.fd() - is CoverResult.Miss -> null + when (val result = settingCovers.obtain(requireNotNull(context), id)) { + is CoverResult.Hit -> result.cover.fd() + else -> null } } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CompatCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/CompatCovers.kt new file mode 100644 index 000000000..8a52e428f --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/CompatCovers.kt @@ -0,0 +1,72 @@ +package org.oxycblt.auxio.image.covers + + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.ParcelFileDescriptor +import android.provider.MediaStore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.cover.CoverResult +import org.oxycblt.musikr.cover.Covers +import org.oxycblt.musikr.cover.FileCover +import org.oxycblt.musikr.cover.MutableCovers +import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.metadata.Metadata + +open class CompatCovers(private val context: Context, private val inner: Covers) : Covers { + override suspend fun obtain(id: String): CoverResult { + when (val innerResult = inner.obtain(id)) { + is CoverResult.Hit -> return CoverResult.Hit(innerResult.cover) + is CoverResult.Miss -> { + if (!id.startsWith("compat:")) return CoverResult.Miss() + val uri = Uri.parse(id.substringAfter("compat:")) + return CoverResult.Hit(CompatCover(context, uri)) + } + } + } +} + +class MutableCompatCovers(private val context: Context, private val inner: MutableCovers) : CompatCovers(context, inner), MutableCovers { + override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { + when (val innerResult = inner.create(file, metadata)) { + is CoverResult.Hit -> return CoverResult.Hit(innerResult.cover) + is CoverResult.Miss -> { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return CoverResult.Miss() + } + val mediaStoreUri = MediaStore.getMediaUri(context, file.uri) ?: return CoverResult.Miss() + val proj = arrayOf(MediaStore.MediaColumns._ID) + val cursor = context.contentResolver.query(mediaStoreUri, proj, null, null, null) + val uri = cursor.use { + if (it == null || !it.moveToFirst()) { + return CoverResult.Miss() + } + val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon().run { + appendPath(id.toString()) + appendPath("albumart") + build() + } + } + return CoverResult.Hit(CompatCover(context, uri)) + } + } + } + + override suspend fun cleanup(excluding: Collection) {} +} + +class CompatCover(private val context: Context, private val uri: Uri) : FileCover { + override val id = "compat:$uri" + + override suspend fun fd(): ParcelFileDescriptor? { + return context.contentResolver.openFileDescriptor(uri, "r") + } + + override suspend fun open() = withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt index 3a9fc4478..46ac9eee7 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt @@ -19,13 +19,14 @@ package org.oxycblt.auxio.image.covers import android.content.Context +import org.apache.commons.lang3.ObjectUtils.Null import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverResult import org.oxycblt.musikr.cover.MutableCovers import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.metadata.Metadata -class NullCovers(private val context: Context) : MutableCovers { +class NullCovers(private val context: Context) : MutableCovers { override suspend fun obtain(id: String) = CoverResult.Hit(NullCover) override suspend fun create(file: DeviceFile, metadata: Metadata) = CoverResult.Hit(NullCover) diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt index 63e1dfd8d..e2f68e6be 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt @@ -23,19 +23,23 @@ import java.util.UUID import javax.inject.Inject import org.oxycblt.auxio.image.CoverMode import org.oxycblt.auxio.image.ImageSettings +import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverIdentifier import org.oxycblt.musikr.cover.CoverParams +import org.oxycblt.musikr.cover.CoverResult +import org.oxycblt.musikr.cover.FileCover import org.oxycblt.musikr.cover.MutableCovers interface SettingCovers { - suspend fun create(context: Context, revision: UUID): MutableCovers + suspend fun obtain(context: Context, id: String): CoverResult + suspend fun mutate(context: Context, revision: UUID): MutableCovers } class SettingCoversImpl @Inject constructor(private val imageSettings: ImageSettings, private val identifier: CoverIdentifier) : SettingCovers { - override suspend fun create(context: Context, revision: UUID): MutableCovers = + override suspend fun mutate(context: Context, revision: UUID): MutableCovers = when (imageSettings.coverMode) { CoverMode.OFF -> NullCovers(context) CoverMode.SAVE_SPACE -> siloedCovers(context, revision, CoverParams.of(500, 70)) @@ -43,6 +47,13 @@ constructor(private val imageSettings: ImageSettings, private val identifier: Co CoverMode.HIGH_QUALITY -> siloedCovers(context, revision, CoverParams.of(1000, 100)) } + override suspend fun obtain(context: Context, id: String): CoverResult { + val coverId = SiloedCoverId.parse(id) ?: return CoverResult.Miss() + val siloedCovers = SiloedCovers.from(context, coverId.silo) + val covers = CompatCovers(context, siloedCovers) + return covers.obtain(coverId.id) + } + private suspend fun siloedCovers(context: Context, revision: UUID, with: CoverParams) = - MutableSiloedCovers.from(context, CoverSilo(revision, with), identifier) + MutableCompatCovers(context, MutableSiloedCovers.from(context, CoverSilo(revision, with), identifier)) } diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt index d116b662c..6ace70edf 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt @@ -35,8 +35,8 @@ import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.app.AppFiles import org.oxycblt.musikr.metadata.Metadata -open class SiloedCovers(private val silo: CoverSilo, private val fileCovers: FileCovers) : Covers { - override suspend fun obtain(id: String): CoverResult { +open class SiloedCovers(private val silo: CoverSilo, private val fileCovers: FileCovers) : Covers { + override suspend fun obtain(id: String): CoverResult { val coverId = SiloedCoverId.parse(id) ?: return CoverResult.Miss() if (coverId.silo != silo) return CoverResult.Miss() return when (val result = fileCovers.obtain(coverId.id)) { @@ -58,8 +58,8 @@ private constructor( private val rootDir: File, private val silo: CoverSilo, private val fileCovers: MutableFileCovers -) : SiloedCovers(silo, fileCovers), MutableCovers { - override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult = +) : SiloedCovers(silo, fileCovers), MutableCovers { + override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult = when (val result = fileCovers.create(file, metadata)) { is CoverResult.Hit -> CoverResult.Hit(SiloedCover(silo, result.cover)) is CoverResult.Miss -> CoverResult.Miss() diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index d305277de..90aac5c36 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -389,7 +389,7 @@ constructor( val currentRevision = musicSettings.revision val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID() val cache = if (withCache) storedCache.visible() else storedCache.invisible() - val covers = settingCovers.create(context, newRevision) + val covers = settingCovers.mutate(context, newRevision) val storage = Storage(cache, covers, storedPlaylists) val interpretation = Interpretation(nameFactory, separators, ignoreHidden) diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt index 9972dda06..f6e505393 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt @@ -22,12 +22,12 @@ import java.io.InputStream import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.metadata.Metadata -interface Covers { - suspend fun obtain(id: String): CoverResult +interface Covers { + suspend fun obtain(id: String): CoverResult } -interface MutableCovers : Covers { - suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult +interface MutableCovers : Covers { + suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult suspend fun cleanup(excluding: Collection) }