From 3de53901402734fd35d6c61910505913eb3b4cdd Mon Sep 17 00:00:00 2001 From: T R Date: Mon, 9 Feb 2026 07:34:44 +1300 Subject: [PATCH] fix: album art now displays on android auto (#414) Co-authored-by: Thomas R Co-authored-by: eddyizm --- app/src/main/AndroidManifest.xml | 7 +- .../tempo/model/SessionMediaItem.kt | 4 +- .../provider/AlbumArtContentProvider.java | 149 ++++++++++++++++++ .../repository/AutomotiveRepository.java | 20 +-- .../tempo/util/MappingUtil.java | 6 +- 5 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b8d72d8b..b7a386b7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -96,7 +96,12 @@ android:resource="@xml/widget_info"/> - + diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt index 60d641ce..39ac3f03 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt @@ -1,5 +1,6 @@ package com.cappielloantonio.tempo.model +import android.content.ContentResolver import android.net.Uri import android.os.Bundle import androidx.annotation.Keep @@ -13,6 +14,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import com.cappielloantonio.tempo.glide.CustomGlideRequest +import com.cappielloantonio.tempo.provider.AlbumArtContentProvider import com.cappielloantonio.tempo.subsonic.models.Child import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode @@ -197,7 +199,7 @@ class SessionMediaItem() { fun getMediaItem(): MediaItem { val uri: Uri = getStreamUri() - val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize())) + val artworkUri = AlbumArtContentProvider.contentUri(coverArtId) val bundle = Bundle() bundle.putString("id", id) diff --git a/app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java b/app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java new file mode 100644 index 00000000..cace2db9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java @@ -0,0 +1,149 @@ +package com.cappielloantonio.tempo.provider; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.util.Preferences; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class AlbumArtContentProvider extends ContentProvider { + public static final String AUTHORITY = "com.cappielloantonio.tempo.provider"; + public static final String ALBUM_ART = "albumArt"; + private ExecutorService executor; + + private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + static { + uriMatcher.addURI(AUTHORITY, "albumArt/*", 1); + } + + public static Uri contentUri(String artworkId) { + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .appendPath(ALBUM_ART) + .appendPath(artworkId) + .build(); + } + + @Nullable + @Override + public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { + Context context = getContext(); + String albumId = uri.getLastPathSegment(); + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize())); + + try { + // use pipe to communicate between background thread and caller of openFile() + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + ParcelFileDescriptor readSide = pipe[0]; + ParcelFileDescriptor writeSide = pipe[1]; + + // perform loading in background thread to avoid blocking UI + executor.execute(() -> { + try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { + + // request artwork from API using Glide + File file = Glide.with(context) + .asFile() + .load(artworkUri) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .submit() + .get(); + + // copy artwork down pipe returned by ContentProvider + try (InputStream in = new FileInputStream(file)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + } catch (Exception e) { + writeSide.closeWithError("Failed to load image: " + e.getMessage()); + } + + } catch (Exception e) { + try { + writeSide.closeWithError("Failed to load image: " + e.getMessage()); + } catch (IOException ignored) {} + } + }); + + return readSide; + + } catch (IOException e) { + throw new FileNotFoundException("Could not create pipe: " + e.getMessage()); + } + } + + @Override + public boolean onCreate() { + executor = Executors.newFixedThreadPool( + Math.max(2, Runtime.getRuntime().availableProcessors() / 2) + ); + return true; + } + + @Override + public void shutdown() { + if (executor != null) { + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + } + } + } + + @Nullable + @Override + public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) { + return null; + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + return ""; + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) { + return null; + } + + @Override + public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) { + return 0; + } + + @Override + public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) { + return 0; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java index fe24d81d..b509360e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java @@ -1,6 +1,7 @@ package com.cappielloantonio.tempo.repository; +import android.content.ContentResolver; import android.net.Uri; import android.view.View; @@ -22,6 +23,7 @@ import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.SessionMediaItem; +import com.cappielloantonio.tempo.provider.AlbumArtContentProvider; import com.cappielloantonio.tempo.service.DownloaderManager; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; @@ -70,7 +72,7 @@ public class AutomotiveRepository { List mediaItems = new ArrayList<>(); for (AlbumID3 album : albums) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId()); MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setTitle(album.getName()) @@ -217,7 +219,7 @@ public class AutomotiveRepository { List mediaItems = new ArrayList<>(); for (AlbumID3 album : albums) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId()); MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setTitle(album.getName()) @@ -272,7 +274,7 @@ public class AutomotiveRepository { List mediaItems = new ArrayList<>(); for (ArtistID3 artist : artists) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize())); + Uri artworkUri = AlbumArtContentProvider.contentUri(artist.getCoverArtId()); MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setTitle(artist.getName()) @@ -397,7 +399,7 @@ public class AutomotiveRepository { List children = response.body().getSubsonicResponse().getIndexes().getChildren(); for (Child song : children) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(song.getCoverArtId(), Preferences.getImageSize())); + Uri artworkUri = AlbumArtContentProvider.contentUri(song.getCoverArtId()); MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setTitle(song.getTitle()) @@ -451,7 +453,7 @@ public class AutomotiveRepository { List mediaItems = new ArrayList<>(); for (Child child : directory.getChildren()) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(child.getCoverArtId(), Preferences.getImageSize())); + Uri artworkUri = AlbumArtContentProvider.contentUri(child.getCoverArtId()); MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setTitle(child.getTitle()) @@ -550,7 +552,7 @@ public class AutomotiveRepository { List mediaItems = new ArrayList<>(); for (PodcastEpisode episode : episodes) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(episode.getCoverArtId(), Preferences.getImageSize())); + Uri artworkUri = AlbumArtContentProvider.contentUri(episode.getCoverArtId()); MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setTitle(episode.getTitle()) @@ -687,7 +689,7 @@ public class AutomotiveRepository { List mediaItems = new ArrayList<>(); for (AlbumID3 album : albums) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId()); MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setTitle(album.getName()) @@ -800,7 +802,7 @@ public class AutomotiveRepository { if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) { for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize())); + Uri artworkUri = AlbumArtContentProvider.contentUri(artist.getCoverArtId()); MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setTitle(artist.getName()) @@ -822,7 +824,7 @@ public class AutomotiveRepository { if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) { for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId()); MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setTitle(album.getName()) diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java index a41e0983..71007c78 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java @@ -1,5 +1,6 @@ package com.cappielloantonio.tempo.util; +import android.content.ContentResolver; import android.net.Uri; import android.os.Bundle; import android.util.Log; @@ -15,6 +16,7 @@ import androidx.media3.common.HeartRating; import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.provider.AlbumArtContentProvider; import com.cappielloantonio.tempo.repository.DownloadRepository; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; @@ -45,7 +47,7 @@ public class MappingUtil { Uri artworkUri = null; if (coverArtId != null) { - artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, Preferences.getImageSize())); + artworkUri = AlbumArtContentProvider.contentUri(coverArtId); } Bundle bundle = new Bundle(); @@ -235,7 +237,7 @@ public class MappingUtil { public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) { Uri uri = getUri(podcastEpisode); - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(podcastEpisode.getCoverArtId(), Preferences.getImageSize())); + Uri artworkUri = AlbumArtContentProvider.contentUri(podcastEpisode.getCoverArtId()); Bundle bundle = new Bundle(); bundle.putString("id", podcastEpisode.getId());