From 7661d7aa4d3d38816f829855e4c6abfc3c0da200 Mon Sep 17 00:00:00 2001 From: Jorilx Date: Sun, 15 Mar 2026 15:40:05 +0100 Subject: [PATCH] Add 'genres' page/function to Android Auto --- .../repository/AutomotiveRepository.java | 89 +++++++++++++++++++ app/src/main/res/drawable/ic_aa_genres.xml | 11 +++ app/src/main/res/values/arrays.xml | 4 +- app/src/main/res/values/strings.xml | 1 + .../tempo/service/MediaBrowserTree.kt | 22 ++++- 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_aa_genres.xml 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 86b6fa38..c4dc829c 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java @@ -35,6 +35,7 @@ import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; import com.cappielloantonio.tempo.subsonic.models.MusicFolder; import com.cappielloantonio.tempo.subsonic.models.Playlist; import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.cappielloantonio.tempo.subsonic.models.Genre; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MusicUtil; @@ -952,6 +953,94 @@ public class AutomotiveRepository { thread.start(); } + public ListenableFuture>> getGenres(String prefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getGenres() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getGenres() != null && response.body().getSubsonicResponse().getGenres().getGenres() != null) { + List genres = response.body().getSubsonicResponse().getGenres().getGenres(); + + // Sort genres alphabetically by name + genres.sort((g1, g2) -> { + String name1 = g1.getGenre() != null ? g1.getGenre() : ""; + String name2 = g2.getGenre() != null ? g2.getGenre() : ""; + return name1.compareToIgnoreCase(name2); + }); + + List mediaItems = new ArrayList<>(); + + for (Genre genre : genres) { + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(genre.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + genre.getGenre()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getSongsByGenre(String genre, int count) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getSongsByGenre(genre, count, 0) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null && response.body().getSubsonicResponse().getSongsByGenre().getSongs() != null) { + List songs = response.body().getSubsonicResponse().getSongsByGenre().getSongs(); + + setChildrenMetadata(songs); + + List mediaItems = MappingUtil.mapMediaItems(songs); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + private static class GetMediaItemThreadSafe implements Runnable { private final SessionMediaItemDao sessionMediaItemDao; private final String id; diff --git a/app/src/main/res/drawable/ic_aa_genres.xml b/app/src/main/res/drawable/ic_aa_genres.xml new file mode 100644 index 00000000..d8b4bf27 --- /dev/null +++ b/app/src/main/res/drawable/ic_aa_genres.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index ebffd85e..a825024f 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -310,8 +310,9 @@ Star tracks Star albums - Star artistes + Star artists Random + Genres -1 @@ -331,6 +332,7 @@ 13 14 15 + 16 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08264392..8768dd1e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Podcast Radio Random + Genres Recent Song played ★ Albums diff --git a/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt b/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt index e9ad100f..5b3fe879 100644 --- a/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt +++ b/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt @@ -50,6 +50,7 @@ object MediaBrowserTree { private const val STARRED_ARTISTS_ID = "[starredArtistsID]" private const val RANDOM_ID = "[randomID]" private const val FOLDER_ID = "[folderID]" + private const val GENRES_ID = "[genresID]" // System functions private const val INDEX_ID = "[indexID]" @@ -178,7 +179,8 @@ object MediaBrowserTree { STARRED_TRACKS_ID, STARRED_ALBUMS_ID, STARRED_ARTISTS_ID, - RANDOM_ID + RANDOM_ID, + GENRES_ID ) // Root level @@ -419,6 +421,19 @@ object MediaBrowserTree { ) ) + treeNodes[GENRES_ID] = + MediaItemNode( + buildMediaItem( + gridView = albumView, + title = appContext.getString(R.string.aa_genres), + mediaId = GENRES_ID, + isPlayable = false, + isBrowsable = true, + imageUri = iconUri(R.drawable.ic_aa_genres), + mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED + ) + ) + val root = treeNodes[ROOT_ID]!! val selectedIds = mutableSetOf() @@ -474,6 +489,7 @@ object MediaBrowserTree { STARRED_ALBUMS_ID -> automotiveRepository.getStarredAlbums(id) STARRED_ARTISTS_ID -> automotiveRepository.getStarredArtists(id) RANDOM_ID -> automotiveRepository.getRandomSongs(100) + GENRES_ID -> automotiveRepository.getGenres(id) else -> { if (id.startsWith(LAST_PLAYED_ID)) { @@ -512,6 +528,10 @@ object MediaBrowserTree { return automotiveRepository.getArtistAlbum(STARRED_ALBUMS_ID,id.removePrefix(STARRED_ARTISTS_ID)) } + if (id.startsWith(GENRES_ID)) { + return automotiveRepository.getSongsByGenre(id.removePrefix(GENRES_ID), 100) + } + if (id.startsWith(PLAYLIST_ID)) { return automotiveRepository.getPlaylistSongs(id.removePrefix(PLAYLIST_ID)) }