From 7661d7aa4d3d38816f829855e4c6abfc3c0da200 Mon Sep 17 00:00:00 2001 From: Jorilx Date: Sun, 15 Mar 2026 15:40:05 +0100 Subject: [PATCH 1/5] 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)) } From 5fc15c717334e652143609fd6a50830da9caf142 Mon Sep 17 00:00:00 2001 From: Joril Date: Mon, 16 Mar 2026 21:02:01 +0000 Subject: [PATCH 2/5] Add 'genres' string to multilingual files that use aa_tab_titles and aa_tab_values --- app/src/main/res/values-fr/arrays.xml | 2 ++ app/src/main/res/values-ru/arrays.xml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/main/res/values-fr/arrays.xml b/app/src/main/res/values-fr/arrays.xml index 54ad8cdf..6994cab7 100644 --- a/app/src/main/res/values-fr/arrays.xml +++ b/app/src/main/res/values-fr/arrays.xml @@ -288,6 +288,7 @@ Albums favoris Artistes favoris Aléatoire + Genres -1 @@ -307,6 +308,7 @@ 13 14 15 + 16 diff --git a/app/src/main/res/values-ru/arrays.xml b/app/src/main/res/values-ru/arrays.xml index 860c31c5..34150efe 100644 --- a/app/src/main/res/values-ru/arrays.xml +++ b/app/src/main/res/values-ru/arrays.xml @@ -297,6 +297,7 @@ Избранные альбомы Избранные артисты Случайное + Жанры -1 @@ -316,6 +317,7 @@ 13 14 15 + 16 From 0b61653dcc307677dabbcf3a7ffeecfd921b6117 Mon Sep 17 00:00:00 2001 From: Joril Date: Mon, 16 Mar 2026 21:11:16 +0000 Subject: [PATCH 3/5] Updated USAGE.md --- USAGE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/USAGE.md b/USAGE.md index 9fd873c1..1592872a 100644 --- a/USAGE.md +++ b/USAGE.md @@ -203,6 +203,7 @@ The Android Auto interface can be configured by user to best suit their preferen - Star albums - Star artists - Random : 100 random songs +- Genres : 100 random songs of the chosen genre If all tabs are set to "Do not display", then "Home" tab will be created with all functions inside. From 1ef7ef737799bb099092f57022d37c8b4214c16a Mon Sep 17 00:00:00 2001 From: Joril Date: Tue, 17 Mar 2026 20:26:07 +0000 Subject: [PATCH 4/5] Add preference to shuffle songs on the 'genre' page --- .../repository/AutomotiveRepository.java | 68 ++++++++++++------- .../tempo/util/Preferences.kt | 11 +++ app/src/main/res/values-fr/strings.xml | 3 +- app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/global_preferences.xml | 7 +- .../tempo/service/MediaBrowserTree.kt | 5 +- 7 files changed, 72 insertions(+), 26 deletions(-) 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 c4dc829c..3d938862 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java @@ -1008,39 +1008,61 @@ public class AutomotiveRepository { return listenableFuture; } - public ListenableFuture>> getSongsByGenre(String genre, int count) { + public ListenableFuture>> getSongsByGenre(String genre, int count, boolean shuffle) { 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(); + Call call; + if (shuffle) { + call = App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getRandomSongs(count, null, null, genre); + } else { + call = App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getSongsByGenre(genre, count, 0); + } - 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)); - } + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + List songs; + if (shuffle) { + songs = response.body().getSubsonicResponse().getRandomSongs() != null + ? response.body().getSubsonicResponse().getRandomSongs().getSongs() + : null; + } else { + songs = response.body().getSubsonicResponse().getSongsByGenre() != null + ? response.body().getSubsonicResponse().getSongsByGenre().getSongs() + : null; } - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - listenableFuture.setException(t); + if (songs != null) { + 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)); } - }); + } 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) { + return getSongsByGenre(genre, count, false); + } + private static class GetMediaItemThreadSafe implements Runnable { private final SessionMediaItemDao sessionMediaItemDao; private final String id; diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index 9f40758c..e05d66a3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -102,6 +102,7 @@ object Preferences { private const val AA_SECOND_TAB = "androidauto_second_tab" private const val AA_THIRD_TAB = "androidauto_third_tab" private const val AA_FOURTH_TAB = "androidauto_fourth_tab" + private const val AA_SHUFFLE_GENRE_SONGS = "androidauto_shuffle_genre_songs" @JvmStatic fun getServer(): String? { @@ -818,4 +819,14 @@ object Preferences { return App.getInstance().preferences.getString(AA_FOURTH_TAB, "3")!!.toInt() } + @JvmStatic + fun isAndroidAutoShuffleGenreSongsEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(AA_SHUFFLE_GENRE_SONGS, false) + } + + @JvmStatic + fun setAndroidAutoShuffleGenreSongsEnabled(enabled: Boolean) { + App.getInstance().preferences.edit().putBoolean(AA_SHUFFLE_GENRE_SONGS, enabled).apply() + } + } \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9cc61079..07639ef9 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -338,6 +338,8 @@ Affichage du deuxième onglet Affichage du troisième onglet Affichage du quatrième onglet + Mélanger les chansons par genre + Lire des chansons aléatoires lors de la sélection d\'un genre Format de transcodage Si activé, Tempus ne forcera pas le téléchargement de la piste avec les paramètres de transcodage ci-dessous. Prioriser les paramètres du serveurs, utilisés pour le streaming, dans les téléchargements @@ -434,7 +436,6 @@ Thème Taille des vignettes Données - Données Géneral Playlist Note diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 9315a180..ee27227c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -402,6 +402,8 @@ Second tab display Third tab display Fourth tab display + Перемешивать треки по жанру + Воспроизводить случайные треки при выборе жанра Показывать качество аудио Битрейт и формат аудио будут отображаться для каждого трека. Показывать рейтинг трека diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8768dd1e..2662e9e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -403,6 +403,8 @@ Second tab display Third tab display Fourth tab display + Shuffle genre songs + Play random songs when selecting a genre Show audio quality The bitrate and audio format will be shown for each audio track. Show song star rating diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index 0b7c53d6..7e90ccf8 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -519,7 +519,12 @@ app:entryValues="@array/aa_tab_values" app:key="androidauto_fourth_tab" app:title="@string/settings_androidauto_fourth_tab" - app:useSimpleSummaryProvider="true" /> + app:useSimpleSummaryProvider="true" /> + + 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 5b3fe879..a417e739 100644 --- a/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt +++ b/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt @@ -529,7 +529,10 @@ object MediaBrowserTree { } if (id.startsWith(GENRES_ID)) { - return automotiveRepository.getSongsByGenre(id.removePrefix(GENRES_ID), 100) + val shuffle = Preferences.isAndroidAutoShuffleGenreSongsEnabled() + // If the user doesn't want random songs, it's likely it's for perusing them, so provide as many as possible + val count = if (shuffle) 100 else 500 + return automotiveRepository.getSongsByGenre(id.removePrefix(GENRES_ID), count, shuffle) } if (id.startsWith(PLAYLIST_ID)) { From 2aa4ccde91dc4e3137f5d2fe9ea2bb494d9bb914 Mon Sep 17 00:00:00 2001 From: Joril Date: Tue, 17 Mar 2026 20:29:58 +0000 Subject: [PATCH 5/5] Updated USAGE.md --- USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/USAGE.md b/USAGE.md index 1592872a..2d9f7a22 100644 --- a/USAGE.md +++ b/USAGE.md @@ -203,7 +203,7 @@ The Android Auto interface can be configured by user to best suit their preferen - Star albums - Star artists - Random : 100 random songs -- Genres : 100 random songs of the chosen genre +- Genres : 500 songs of the chosen genre OR 100 random songs if "shuffle genre songs" is selected If all tabs are set to "Do not display", then "Home" tab will be created with all functions inside.