fix: album art now displays on android auto (#414)

Co-authored-by: Thomas R <tdr@thomasr.co>
Co-authored-by: eddyizm <eddyizm@gmail.com>
This commit is contained in:
T R 2026-02-09 07:34:44 +13:00 committed by GitHub
parent d215581e19
commit 3de5390140
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 173 additions and 13 deletions

View file

@ -96,7 +96,12 @@
android:resource="@xml/widget_info"/>
</receiver>
<provider
android:name=".provider.AlbumArtContentProvider"
android:authorities="com.cappielloantonio.tempo.provider"
android:enabled="true"
android:exported="true"
/>
</application>
</manifest>

View file

@ -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)

View file

@ -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;
}
}

View file

@ -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<MediaItem> 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<MediaItem> 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<MediaItem> 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<Child> 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<MediaItem> 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<MediaItem> 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<MediaItem> 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())

View file

@ -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());