Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,9 @@
<meta-data
android:name="com.google.android.gms.car.application.theme"
android:resource="@style/Theme.Gramophone"/>
<!-- TODO uncomment to enable Android Auto when library browsing is implemented
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
-->
<activity
android:name=".logic.ui.BugHandlerActivity"
android:exported="false"
Expand Down Expand Up @@ -162,6 +160,12 @@
android:resource="@xml/file_paths" />
</provider>

<provider
android:name=".logic.GramophoneAlbumArtProvider"
android:authorities="${applicationId}.albumart"
android:exported="true"
android:permission="com.google.android.finsky.permission.GEARHEAD_SERVICE" />

<receiver android:name=".ui.LyricWidgetProvider"
android:exported="false">
<intent-filter>
Expand Down Expand Up @@ -237,4 +241,4 @@
</intent>
</queries>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.akanework.gramophone.logic

import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import org.akanework.gramophone.logic.utils.ArtCacheManager

/**
* ContentProvider that serves album artwork to external processes (e.g. Android Auto).
*
* External processes cannot resolve Gramophone's internal URI schemes
* (`gramophoneSongCover://`, `gramophoneAlbumCover://`). This provider acts as a bridge,
* using the shared [ArtCacheManager] to resolve, cache and serve the artwork over a
* standard `content://` URI.
*
* URI format: `content://org.akanework.gramophone.albumart/{type}/{id}/{encodedPath}`
* where `type` is "song" or "album".
*/
class GramophoneAlbumArtProvider : ContentProvider() {

override fun onCreate() = true

override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
val context = context ?: return null
return ArtCacheManager.openFileDescriptor(context, uri)
}

override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? = null

override fun getType(uri: Uri): String = "image/jpeg"

override fun insert(uri: Uri, values: ContentValues?): Uri? = null

override fun delete(
uri: Uri,
selection: String?,
selectionArgs: Array<out String>?
): Int = 0

override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int = 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,14 @@
import android.annotation.SuppressLint
import android.app.Application
import android.app.NotificationManager
import android.content.ContentUris
import android.content.Intent
import android.content.SharedPreferences
import android.media.ThumbnailUtils
import android.os.Build
import android.os.Debug
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import android.provider.MediaStore
import android.util.Size

import android.webkit.MimeTypeMap
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composer
Expand All @@ -39,6 +36,7 @@
import androidx.media3.common.util.Log
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.preference.PreferenceManager
import androidx.core.net.toUri
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
Expand All @@ -52,6 +50,7 @@
import coil3.fetch.SourceFetchResult
import coil3.request.NullRequestDataException
import coil3.size.pxOrElse
import coil3.size.pxOrElse
import coil3.toCoilUri
import coil3.util.Logger
import kotlinx.coroutines.CoroutineScope
Expand All @@ -70,7 +69,7 @@
import org.akanework.gramophone.ui.LyricWidgetProvider
import org.lsposed.hiddenapibypass.LSPass
import org.nift4.gramophone.hificore.UacManager
import uk.akane.libphonograph.Constants
import org.akanework.gramophone.logic.utils.ArtCacheManager
import uk.akane.libphonograph.reader.FlowReader
import uk.akane.libphonograph.utils.MiscUtils
import java.io.File
Expand All @@ -82,10 +81,6 @@

companion object {
private const val TAG = "GramophoneApplication"

// not actually defined in API, but CTS tested
// https://cs.android.com/android/platform/superproject/main/+/main:packages/providers/MediaProvider/src/com/android/providers/media/LocalUriMatcher.java;drc=ddf0d00b2b84b205a2ab3581df8184e756462e8d;l=182
private const val MEDIA_ALBUM_ART = "albumart"
}

init {
Expand Down Expand Up @@ -263,79 +258,21 @@
.components {
add(Fetcher.Factory { data, options, _ ->
if (data !is Uri) return@Factory null
if (data.scheme != "gramophoneSongCover") return@Factory null
return@Factory Fetcher {
val file = File(data.path!!)
val uri = ContentUris.appendId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon(),
data.authority!!.toLong()
).appendPath(MEDIA_ALBUM_ART).build()
val bmp = if (options.size.width.pxOrElse { 0 } > 300
&& options.size.height.pxOrElse { 0 } > 300) try {
if (hasScopedStorageV1()) {
ThumbnailUtils.createAudioThumbnail(file, options.size.let {
Size(
it.width.pxOrElse { throw IllegalArgumentException("missing required size") },
it.height.pxOrElse { throw IllegalArgumentException("missing required size") })
}, null)
} else null // TODO: fallback for <Q?
} catch (e: IOException) {
if (e.message != "No embedded album art found" &&
e.message != "No thumbnails in Downloads directories" &&
e.message != "No thumbnails in top-level directories" &&
e.message != "No album art found"
)
throw e
null
} else null
if (bmp != null) {
ImageFetchResult(
bmp.asImage(), true, DataSource.DISK
)
} else {
if (uri == null) return@Fetcher null
val stream = contentResolver.openAssetFileDescriptor(uri, "r")
checkNotNull(stream) { "Unable to open '$uri'." }
SourceFetchResult(
source = ImageSource(
source = stream.createInputStream().source().buffer(),
fileSystem = options.fileSystem,
metadata = ContentMetadata(uri.toCoilUri(), stream),
),
mimeType = contentResolver.getType(uri),
dataSource = DataSource.DISK,
)
}
}
})
add(Fetcher.Factory { data, options, _ ->
if (data !is Uri) return@Factory null
if (data.scheme != "gramophoneAlbumCover") return@Factory null
if (data.scheme != "gramophoneSongCover" && data.scheme != "gramophoneAlbumCover") return@Factory null
return@Factory Fetcher {
val cover = MiscUtils.findBestCover(File(data.path!!))
if (cover == null) {
val uri =
ContentUris.withAppendedId(
Constants.baseAlbumCoverUri,
data.authority!!.toLong()
)
val contentResolver = options.context.contentResolver
val afd = contentResolver.openAssetFileDescriptor(uri, "r")
checkNotNull(afd) { "Unable to open '$uri'." }
return@Fetcher SourceFetchResult(
source = ImageSource(
source = afd.createInputStream().source().buffer(),
fileSystem = options.fileSystem,
metadata = ContentMetadata(data, afd),
),
mimeType = contentResolver.getType(uri),
dataSource = DataSource.DISK,
)
}
return@Fetcher SourceFetchResult(
ImageSource(cover.toOkioPath(), options.fileSystem, null, null, null),
MimeTypeMap.getSingleton().getMimeTypeFromExtension(cover.extension),
DataSource.DISK
val requestWidth = options.size.width.pxOrElse { 0 }
val requestHeight = options.size.height.pxOrElse { 0 }
val size = if (requestWidth > 0 && requestWidth <= 300 && requestHeight > 0 && requestHeight <= 300) 300 else 1024

val art = ArtCacheManager.getArt(options.context, data.toString().toUri(), size)
checkNotNull(art) { "Unable to open '$data'." }
SourceFetchResult(
source = ImageSource(
source = art.file.inputStream().source().buffer(),
fileSystem = options.fileSystem,
),
mimeType = art.mimeType,
dataSource = DataSource.DISK,

Check notice on line 275 in app/src/main/java/org/akanework/gramophone/logic/GramophoneApplication.kt

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (beta)

✅ Getting better: Complex Method

GramophoneApplication.newImageLoader decreases in cyclomatic complexity from 27 to 16, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
)
}
})
Expand Down
Loading