From 0f5a8f6b976448a02ebdebcc4018631ed53573c8 Mon Sep 17 00:00:00 2001 From: Jorilx Date: Wed, 25 Mar 2026 15:13:24 +0100 Subject: [PATCH] Add 'genres' page/function to Android Auto (#505) * Add 'genres' page/function to Android Auto * Add 'genres' string to multilingual files that use aa_tab_titles and aa_tab_values * Updated USAGE.md * Add preference to shuffle songs on the 'genre' page * Updated USAGE.md --- USAGE.md | 1 + .../repository/AutomotiveRepository.java | 111 ++++++++++++++++++ .../tempo/util/Preferences.kt | 11 ++ app/src/main/res/drawable/ic_aa_genres.xml | 11 ++ app/src/main/res/values-fr/arrays.xml | 2 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-ru/arrays.xml | 2 + app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values/arrays.xml | 4 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/global_preferences.xml | 7 +- .../tempo/service/MediaBrowserTree.kt | 25 +++- 12 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/drawable/ic_aa_genres.xml diff --git a/USAGE.md b/USAGE.md index 9fd873c1..2d9f7a22 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 : 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. 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..3d938862 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,116 @@ 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, boolean shuffle) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + Call call; + if (shuffle) { + call = App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getRandomSongs(count, null, null, genre); + } else { + call = App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getSongsByGenre(genre, count, 0); + } + + 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; + } + + 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/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-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-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7d441df1..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 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 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/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 bcf80ebb..874fb5a6 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 @@ -402,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 e9ad100f..a417e739 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,13 @@ object MediaBrowserTree { return automotiveRepository.getArtistAlbum(STARRED_ALBUMS_ID,id.removePrefix(STARRED_ARTISTS_ID)) } + if (id.startsWith(GENRES_ID)) { + 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)) { return automotiveRepository.getPlaylistSongs(id.removePrefix(PLAYLIST_ID)) }