From 3c1975f6bf86a0efb2197b9235485e510f9c22b0 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Fri, 26 Dec 2025 17:03:41 -0800 Subject: [PATCH 01/25] wip: initial refactor of instant mix in to be used everywhere else --- .../tempo/repository/SongRepository.java | 334 +++++++++--------- 1 file changed, 174 insertions(+), 160 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java index a40b3c97..4457d37b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -6,17 +6,22 @@ import androidx.lifecycle.MutableLiveData; import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class SongRepository { - private static final String TAG = "SongRepository"; + + public interface MediaCallbackInternal { + void onSongsAvailable(List songs); + } public MutableLiveData> getStarredSongs(boolean random, int size) { MutableLiveData> starredSongs = new MutableLiveData<>(Collections.emptyList()); @@ -42,219 +47,228 @@ public class SongRepository { } @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } + public void onFailure(@NonNull Call call, @NonNull Throwable t) {} }); return starredSongs; } + /** + * Used by ViewModels. Updates the LiveData list incrementally as songs are found. + */ public MutableLiveData> getInstantMix(String id, int count) { - MutableLiveData> instantMix = new MutableLiveData<>(); + MutableLiveData> instantMix = new MutableLiveData<>(new ArrayList<>()); - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSimilarSongs2(id, count) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) { - instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - instantMix.setValue(null); - } - }); + performSmartMix(id, count, songs -> { + List current = instantMix.getValue(); + if (current != null) { + for (Child s : songs) { + if (!current.contains(s)) current.add(s); + } + instantMix.postValue(current); + } + }); return instantMix; } - public MutableLiveData> getRandomSample(int number, Integer fromYear, Integer toYear) { - MutableLiveData> randomSongsSample = new MutableLiveData<>(); + /** + * Overloaded method used by other Repositories + */ + public void getInstantMix(String id, int count, MediaCallbackInternal callback) { + performSmartMix(id, count, callback); + } + private void performSmartMix(final String id, final int count, final MediaCallbackInternal callback) { App.getSubsonicClientInstance(false) - .getAlbumSongListClient() - .getRandomSongs(number, fromYear, toYear) + .getBrowsingClient() + .getSimilarSongs(id, count) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = new ArrayList<>(); - - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) { - songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs()); + List songs = extractSongs(response, "similarSongs"); + if (!songs.isEmpty()) { + callback.onSongsAvailable(songs); + } + if (songs.size() < count / 2) { + fetchContextAndSeed(id, count, callback); } - - randomSongsSample.setValue(songs); } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - + fetchContextAndSeed(id, count, callback); } }); + } + private void fetchContextAndSeed(final String id, final int count, final MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getAlbum(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { + List albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); + if (albumSongs != null && !albumSongs.isEmpty()) { + callback.onSongsAvailable(new ArrayList<>(albumSongs)); + String seedArtistId = albumSongs.get(0).getArtistId(); + fetchSimilarByArtist(seedArtistId, count, callback); + return; + } + } + fillWithRandom(count, callback); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } + }); + } + + private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSimilarSongs2(artistId, count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List similar = extractSongs(response, "similarSongs2"); + if (!similar.isEmpty()) { + callback.onSongsAvailable(similar); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); + } + + private void fillWithRandom(int target, final MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getRandomSongs(target, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + callback.onSongsAvailable(extractSongs(response, "randomSongs")); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); + } + + private List extractSongs(Response response, String type) { + if (response.isSuccessful() && response.body() != null) { + SubsonicResponse res = response.body().getSubsonicResponse(); + List list = null; + if (type.equals("similarSongs") && res.getSimilarSongs() != null) { + list = res.getSimilarSongs().getSongs(); + } else if (type.equals("similarSongs2") && res.getSimilarSongs2() != null) { + list = res.getSimilarSongs2().getSongs(); + } else if (type.equals("randomSongs") && res.getRandomSongs() != null) { + list = res.getRandomSongs().getSongs(); + } + return (list != null) ? list : new ArrayList<>(); + } + return new ArrayList<>(); + } + public MutableLiveData> getRandomSample(int number, Integer fromYear, Integer toYear) { + MutableLiveData> randomSongsSample = new MutableLiveData<>(); + App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) { + songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs())); + } + randomSongsSample.setValue(songs); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); return randomSongsSample; } public MutableLiveData> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) { MutableLiveData> randomSongsSample = new MutableLiveData<>(); - - App.getSubsonicClientInstance(false) - .getAlbumSongListClient() - .getRandomSongs(number, fromYear, toYear, genre) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = new ArrayList<>(); - - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) { - songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs()); - } - - randomSongsSample.setValue(songs); - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - + App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear, genre).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) { + songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs())); + } + randomSongsSample.setValue(songs); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); return randomSongsSample; } public void scrobble(String id, boolean submission) { - App.getSubsonicClientInstance(false) - .getMediaAnnotationClient() - .scrobble(id, submission) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + App.getSubsonicClientInstance(false).getMediaAnnotationClient().scrobble(id, submission).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) {} + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); } public void setRating(String id, int rating) { - App.getSubsonicClientInstance(false) - .getMediaAnnotationClient() - .setRating(id, rating) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + App.getSubsonicClientInstance(false).getMediaAnnotationClient().setRating(id, rating).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) {} + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); } public MutableLiveData> getSongsByGenre(String id, int page) { MutableLiveData> songsByGenre = new MutableLiveData<>(); - - App.getSubsonicClientInstance(false) - .getAlbumSongListClient() - .getSongsByGenre(id, 100, 100 * page) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) { - songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - + App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 100, 100 * page).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) { + songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs()); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); return songsByGenre; } public MutableLiveData> getSongsByGenres(ArrayList genresId) { MutableLiveData> songsByGenre = new MutableLiveData<>(); - - for (String id : genresId) - App.getSubsonicClientInstance(false) - .getAlbumSongListClient() - .getSongsByGenre(id, 500, 0) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = new ArrayList<>(); - - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) { - songs.addAll(response.body().getSubsonicResponse().getSongsByGenre().getSongs()); - } - - songsByGenre.setValue(songs); - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - + for (String id : genresId) { + App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 500, 0).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) { + songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getSongsByGenre().getSongs())); + } + songsByGenre.setValue(songs); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); + } return songsByGenre; } public MutableLiveData getSong(String id) { MutableLiveData song = new MutableLiveData<>(); - - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSong(id) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null) { - song.setValue(response.body().getSubsonicResponse().getSong()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - + App.getSubsonicClientInstance(false).getBrowsingClient().getSong(id).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + song.setValue(response.body().getSubsonicResponse().getSong()); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); return song; } public MutableLiveData getSongLyrics(Child song) { MutableLiveData lyrics = new MutableLiveData<>(null); - - App.getSubsonicClientInstance(false) - .getMediaRetrievalClient() - .getLyrics(song.getArtist(), song.getTitle()) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) { - lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - + App.getSubsonicClientInstance(false).getMediaRetrievalClient().getLyrics(song.getArtist(), song.getTitle()).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) { + lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue()); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); return lyrics; } -} +} \ No newline at end of file From 2624f396e5c94f4cb0e5f2cd42518be05402cfd3 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Fri, 26 Dec 2025 22:08:07 -0800 Subject: [PATCH 02/25] wip: refactor album repo to use the song repo instant mix --- .../tempo/repository/AlbumRepository.java | 40 +++++-------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java index c3533549..7de91d18 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java @@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.repository; import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; + import android.util.Log; import com.cappielloantonio.tempo.App; @@ -204,38 +205,15 @@ public class AlbumRepository { return albumInfo; } - public void getInstantMix(AlbumID3 album, int count, MediaCallback callback) { - Log.d("AlbumRepository", "Attempting getInstantMix for AlbumID: " + album.getId()); - - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSimilarSongs2(album.getId(), count) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = new ArrayList<>(); + public void getInstantMix(AlbumID3 album, int count, final MediaCallback callback) { + Log.d("AlbumRepository", "Starting Instant Mix for album: " + album.getName()); - if (response.isSuccessful() - && response.body() != null - && response.body().getSubsonicResponse().getSimilarSongs2() != null) { - - List similarSongs = response.body().getSubsonicResponse().getSimilarSongs2().getSongs(); - - if (similarSongs == null) { - Log.w("AlbumRepository", "API successful but 'songs' list was NULL for AlbumID: " + album.getId()); - } else { - songs.addAll(similarSongs); - } - } + new SongRepository().getInstantMix(album.getId(), count, songs -> { - callback.onLoadMedia(songs); - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - callback.onLoadMedia(new ArrayList<>()); - } - }); + if (songs != null && !songs.isEmpty()) { + callback.onLoadMedia(songs); + } + }); } public MutableLiveData> getDecades() { @@ -248,7 +226,7 @@ public class AlbumRepository { @Override public void onLoadYear(int last) { if (first != -1 && last != -1) { - List decadeList = new ArrayList(); + List decadeList = new ArrayList<>(); int startDecade = first - (first % 10); int lastDecade = last - (last % 10); From 3a30b3d3792c19d1094da000175db9b221967de3 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Fri, 26 Dec 2025 22:12:53 -0800 Subject: [PATCH 03/25] feat: finishing up album bottom sheet dialog updates for instant mix refactor --- .../AlbumBottomSheetDialog.java | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index a6167eed..5db3add0 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -43,7 +43,6 @@ import com.cappielloantonio.tempo.util.ExternalAudioReader; import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel; import com.cappielloantonio.tempo.viewmodel.HomeViewModel; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import com.google.android.material.snackbar.Snackbar; import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; @@ -114,33 +113,36 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null); - favoriteToggle.setOnClickListener(v -> { - albumBottomSheetViewModel.setFavorite(requireContext()); - }); + favoriteToggle.setOnClickListener(v -> albumBottomSheetViewModel.setFavorite(requireContext())); TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { - AlbumRepository albumRepository = new AlbumRepository(); - albumRepository.getInstantMix(album, 20, new MediaCallback() { - @Override - public void onError(Exception exception) { - exception.printStackTrace(); + AlbumRepository albumRepository = new AlbumRepository(); + albumRepository.getInstantMix(album, 20, new MediaCallback() { + @Override + public void onError(Exception exception) { + exception.printStackTrace(); + } + + @Override + public void onLoadMedia(List media) { + if (!isAdded() || getActivity() == null) { + return; } - @Override - public void onLoadMedia(List media) { - MusicUtil.ratingFilter((ArrayList) media); + MusicUtil.ratingFilter((ArrayList) media); - if (!media.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); - ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + if (!media.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).setBottomSheetInPeek(true); } - - dismissBottomSheet(); } - }); - }); + dismissBottomSheet(); + } + }); + }); TextView playRandom = view.findViewById(R.id.play_random_text_view); playRandom.setOnClickListener(v -> { AlbumRepository albumRepository = new AlbumRepository(); @@ -186,18 +188,16 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements }); TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view); - addToPlaylist.setOnClickListener(v -> { - albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { - Bundle bundle = new Bundle(); - bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs)); + addToPlaylist.setOnClickListener(v -> albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs)); - PlaylistChooserDialog dialog = new PlaylistChooserDialog(); - dialog.setArguments(bundle); - dialog.show(requireActivity().getSupportFragmentManager(), null); + PlaylistChooserDialog dialog = new PlaylistChooserDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); - dismissBottomSheet(); - }); - }); + dismissBottomSheet(); + })); removeAllTextView = view.findViewById(R.id.remove_all_text_view); albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { From d034171d92889d30c4216953aa5fb0b6d28cd954 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 08:17:16 -0800 Subject: [PATCH 04/25] chore: formatting --- .../com/cappielloantonio/tempo/repository/SongRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java index 4457d37b..dfb0565b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -172,6 +172,7 @@ public class SongRepository { } return new ArrayList<>(); } + public MutableLiveData> getRandomSample(int number, Integer fromYear, Integer toYear) { MutableLiveData> randomSongsSample = new MutableLiveData<>(); App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear).enqueue(new Callback() { From f59f572e5ce1f61192ca46bdfb5462a9ccf8f8ca Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 11:04:43 -0800 Subject: [PATCH 05/25] wip: added queue type to for instant mix calls --- app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt index c6a4e3a4..1cbf7495 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -133,4 +133,7 @@ object Constants { const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF" const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE" const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL" + enum class SEEDTYPE { + ARTIST, ALBUM, TRACK + } } \ No newline at end of file From 8de9aff1f6fdaddb05fcd8f1e708d69dda67966a Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 12:27:18 -0800 Subject: [PATCH 06/25] wip: refactor song repo instant mix to take in a type --- .../tempo/repository/SongRepository.java | 134 +++++++++++------- .../tempo/service/MediaManager.java | 3 +- .../tempo/viewmodel/HomeViewModel.java | 3 +- .../viewmodel/PlayerBottomSheetViewModel.java | 2 +- .../viewmodel/SongBottomSheetViewModel.java | 3 +- 5 files changed, 93 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java index dfb0565b..900a4bee 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -7,6 +7,7 @@ import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse; +import com.cappielloantonio.tempo.util.Constants; import java.util.ArrayList; import java.util.Collections; @@ -56,16 +57,26 @@ public class SongRepository { /** * Used by ViewModels. Updates the LiveData list incrementally as songs are found. */ - public MutableLiveData> getInstantMix(String id, int count) { + public MutableLiveData> getInstantMix(String id, Constants.SEEDTYPE type, int count) { MutableLiveData> instantMix = new MutableLiveData<>(new ArrayList<>()); - performSmartMix(id, count, songs -> { + performSmartMix(id, type, count, songs -> { List current = instantMix.getValue(); if (current != null) { for (Child s : songs) { if (!current.contains(s)) current.add(s); } - instantMix.postValue(current); + + if (current.size() < count / 2) { + fillWithRandom(count - current.size(), remainder -> { + for (Child r : remainder) { + if (!current.contains(r)) current.add(r); + } + instantMix.postValue(current); + }); + } else { + instantMix.postValue(current); + } } }); @@ -75,57 +86,79 @@ public class SongRepository { /** * Overloaded method used by other Repositories */ - public void getInstantMix(String id, int count, MediaCallbackInternal callback) { - performSmartMix(id, count, callback); + public void getInstantMix(String id, Constants.SEEDTYPE type, int count, MediaCallbackInternal callback) { + performSmartMix(id, type, count, callback); } - private void performSmartMix(final String id, final int count, final MediaCallbackInternal callback) { - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSimilarSongs(id, count) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = extractSongs(response, "similarSongs"); - if (!songs.isEmpty()) { - callback.onSongsAvailable(songs); - } - if (songs.size() < count / 2) { - fetchContextAndSeed(id, count, callback); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - fetchContextAndSeed(id, count, callback); - } - }); + private void performSmartMix(final String id, final Constants.SEEDTYPE type, final int count, final MediaCallbackInternal callback) { + switch (type) { + case ARTIST: + fetchSimilarByArtist(id, count, callback); + break; + case ALBUM: + fetchAlbumSongsThenSimilar(id, count, callback); + break; + case TRACK: + fetchSingleTrackThenSimilar(id, count, callback); + break; + } } - private void fetchContextAndSeed(final String id, final int count, final MediaCallbackInternal callback) { - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getAlbum(id) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { - List albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); - if (albumSongs != null && !albumSongs.isEmpty()) { - callback.onSongsAvailable(new ArrayList<>(albumSongs)); - String seedArtistId = albumSongs.get(0).getArtistId(); - fetchSimilarByArtist(seedArtistId, count, callback); - return; - } - } - fillWithRandom(count, callback); + private void fetchAlbumSongsThenSimilar(String albumId, int count, MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false).getBrowsingClient().getAlbum(albumId).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { + List albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); + if (albumSongs != null && !albumSongs.isEmpty()) { + callback.onSongsAvailable(new ArrayList<>(albumSongs)); + fetchSimilarByArtist(albumSongs.get(0).getArtistId(), count, callback); + return; } + } + fillWithRandom(count, callback); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } + }); + } - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - fillWithRandom(count, callback); + private void fetchSingleTrackThenSimilar(String trackId, int count, MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false).getBrowsingClient().getSong(trackId).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + Child song = response.body().getSubsonicResponse().getSong(); + if (song != null) { + callback.onSongsAvailable(Collections.singletonList(song)); + fetchSimilarOnly(trackId, count, callback); + return; } - }); + } + fillWithRandom(count, callback); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } + }); + } + + private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = extractSongs(response, "similarSongs"); + if (!songs.isEmpty()) { + callback.onSongsAvailable(songs); + } else { + fillWithRandom(count, callback); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } + }); } private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) { @@ -138,9 +171,14 @@ public class SongRepository { List similar = extractSongs(response, "similarSongs2"); if (!similar.isEmpty()) { callback.onSongsAvailable(similar); + } else { + fillWithRandom(count, callback); } } - @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } }); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java index 02cbd239..e7b6c6e6 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -26,6 +26,7 @@ import com.cappielloantonio.tempo.repository.SongRepository; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; @@ -442,7 +443,7 @@ public class MediaManager { if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) { Preferences.setLastInstantMix(); - LiveData> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10); + LiveData> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, Constants.SEEDTYPE.TRACK, 10); instantMix.observeForever(new Observer>() { @Override public void onChanged(List media) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java index 0a9892fc..e0f13edd 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java @@ -24,6 +24,7 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Playlist; import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Preferences; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; @@ -223,7 +224,7 @@ public class HomeViewModel extends AndroidViewModel { public LiveData> getMediaInstantMix(LifecycleOwner owner, Child media) { mediaInstantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), 20).observe(owner, mediaInstantMix::postValue); + songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, mediaInstantMix::postValue); return mediaInstantMix; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java index df571690..8b631977 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java @@ -278,7 +278,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { public LiveData> getMediaInstantMix(LifecycleOwner owner, Child media) { instantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue); + songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, instantMix::postValue); return instantMix; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java index de379dcd..6ea4a9ab 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java @@ -21,6 +21,7 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.NetworkUtil; @@ -128,7 +129,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel { public LiveData> getInstantMix(LifecycleOwner owner, Child media) { instantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue); + songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, instantMix::postValue); return instantMix; } From 844b57054b40629df061f0098c5bca73b8b44a7c Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 12:31:01 -0800 Subject: [PATCH 07/25] wip: updated album repo for song instant mix type update --- .../com/cappielloantonio/tempo/repository/AlbumRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java index 7de91d18..fd47c7cf 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java @@ -12,6 +12,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumInfo; import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.Constants; import java.util.ArrayList; import java.util.Calendar; @@ -208,7 +209,7 @@ public class AlbumRepository { public void getInstantMix(AlbumID3 album, int count, final MediaCallback callback) { Log.d("AlbumRepository", "Starting Instant Mix for album: " + album.getName()); - new SongRepository().getInstantMix(album.getId(), count, songs -> { + new SongRepository().getInstantMix(album.getId(), Constants.SEEDTYPE.ALBUM, count, songs -> { if (songs != null && !songs.isEmpty()) { callback.onLoadMedia(songs); From f22aea7b1dd63aadf0c2c7337ac89bcde8688249 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 17:46:16 -0800 Subject: [PATCH 08/25] wip: changed seedtype constant to camelcase, updated references --- .../tempo/repository/ArtistRepository.java | 23 +++---------------- .../tempo/repository/SongRepository.java | 8 +++---- .../tempo/service/MediaManager.java | 4 ++-- .../tempo/subsonic/models/SimilarSongs.kt | 2 ++ .../cappielloantonio/tempo/util/Constants.kt | 2 +- .../tempo/viewmodel/HomeViewModel.java | 5 ++-- .../viewmodel/PlayerBottomSheetViewModel.java | 3 +-- .../viewmodel/SongBottomSheetViewModel.java | 4 ++-- 8 files changed, 17 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java index 5bea391f..d823958d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java @@ -11,6 +11,7 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.IndexID3; +import com.cappielloantonio.tempo.util.Constants; import java.util.ArrayList; import java.util.Arrays; @@ -287,26 +288,8 @@ public class ArtistRepository { } public MutableLiveData> getInstantMix(ArtistID3 artist, int count) { - MutableLiveData> instantMix = new MutableLiveData<>(); - - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSimilarSongs2(artist.getId(), count) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) { - instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - - return instantMix; + // Delegate to the centralized SongRepository + return new SongRepository().getInstantMix(artist.getId(), Constants.SEEDTYPE.ARTIST, count); } public MutableLiveData> getRandomSong(ArtistID3 artist, int count) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java index 900a4bee..5d79e283 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -7,7 +7,7 @@ import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import java.util.ArrayList; import java.util.Collections; @@ -57,7 +57,7 @@ public class SongRepository { /** * Used by ViewModels. Updates the LiveData list incrementally as songs are found. */ - public MutableLiveData> getInstantMix(String id, Constants.SEEDTYPE type, int count) { + public MutableLiveData> getInstantMix(String id, SeedType type, int count) { MutableLiveData> instantMix = new MutableLiveData<>(new ArrayList<>()); performSmartMix(id, type, count, songs -> { @@ -86,11 +86,11 @@ public class SongRepository { /** * Overloaded method used by other Repositories */ - public void getInstantMix(String id, Constants.SEEDTYPE type, int count, MediaCallbackInternal callback) { + public void getInstantMix(String id, SeedType type, int count, MediaCallbackInternal callback) { performSmartMix(id, type, count, callback); } - private void performSmartMix(final String id, final Constants.SEEDTYPE type, final int count, final MediaCallbackInternal callback) { + private void performSmartMix(final String id, final SeedType type, final int count, final MediaCallbackInternal callback) { switch (type) { case ARTIST: fetchSimilarByArtist(id, count, callback); diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java index e7b6c6e6..f3139186 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -26,7 +26,7 @@ import com.cappielloantonio.tempo.repository.SongRepository; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; @@ -443,7 +443,7 @@ public class MediaManager { if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) { Preferences.setLastInstantMix(); - LiveData> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, Constants.SEEDTYPE.TRACK, 10); + LiveData> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, SeedType.TRACK, 10); instantMix.observeForever(new Observer>() { @Override public void onChanged(List media) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt index d9bb2053..23a0ffea 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt @@ -1,8 +1,10 @@ package com.cappielloantonio.tempo.subsonic.models import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName @Keep class SimilarSongs { + @SerializedName("song") var songs: List? = null } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt index 1cbf7495..2ae3dbb0 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -133,7 +133,7 @@ object Constants { const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF" const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE" const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL" - enum class SEEDTYPE { + enum class SeedType { ARTIST, ALBUM, TRACK } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java index e0f13edd..86d1f456 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java @@ -24,7 +24,7 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Playlist; import com.cappielloantonio.tempo.subsonic.models.Share; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import com.cappielloantonio.tempo.util.Preferences; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; @@ -35,7 +35,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; -import java.util.stream.Collectors; public class HomeViewModel extends AndroidViewModel { private static final String TAG = "HomeViewModel"; @@ -224,7 +223,7 @@ public class HomeViewModel extends AndroidViewModel { public LiveData> getMediaInstantMix(LifecycleOwner owner, Child media) { mediaInstantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, mediaInstantMix::postValue); + songRepository.getInstantMix(media.getId(), SeedType.TRACK, 20).observe(owner, mediaInstantMix::postValue); return mediaInstantMix; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java index 8b631977..19ced999 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java @@ -13,7 +13,6 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.media3.common.util.UnstableApi; -import androidx.media3.session.MediaBrowser; import com.cappielloantonio.tempo.interfaces.StarCallback; import com.cappielloantonio.tempo.model.Download; @@ -278,7 +277,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { public LiveData> getMediaInstantMix(LifecycleOwner owner, Child media) { instantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, instantMix::postValue); + songRepository.getInstantMix(media.getId(), Constants.SeedType.TRACK, 20).observe(owner, instantMix::postValue); return instantMix; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java index 6ea4a9ab..efa74e90 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java @@ -21,7 +21,7 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Share; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.NetworkUtil; @@ -129,7 +129,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel { public LiveData> getInstantMix(LifecycleOwner owner, Child media) { instantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, instantMix::postValue); + songRepository.getInstantMix(media.getId(), SeedType.TRACK, 20).observe(owner, instantMix::postValue); return instantMix; } From 17020e519295b021ff3419cfa8b43b2766c7f3bb Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 17:48:06 -0800 Subject: [PATCH 09/25] wip: album tracks working, album bottom sheet only pulling in the album, not quite right --- .../cappielloantonio/tempo/repository/AlbumRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java index fd47c7cf..dd1a6401 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java @@ -12,7 +12,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumInfo; import com.cappielloantonio.tempo.subsonic.models.Child; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import java.util.ArrayList; import java.util.Calendar; @@ -209,7 +209,7 @@ public class AlbumRepository { public void getInstantMix(AlbumID3 album, int count, final MediaCallback callback) { Log.d("AlbumRepository", "Starting Instant Mix for album: " + album.getName()); - new SongRepository().getInstantMix(album.getId(), Constants.SEEDTYPE.ALBUM, count, songs -> { + new SongRepository().getInstantMix(album.getId(), SeedType.ALBUM, count, songs -> { if (songs != null && !songs.isEmpty()) { callback.onLoadMedia(songs); From 3b3f55c5de837c99e2609e6d4b51c99ea712e01d Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 17:50:53 -0800 Subject: [PATCH 10/25] wip: point artist repo instant mix to song repo --- .../cappielloantonio/tempo/repository/ArtistRepository.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java index d823958d..7cd69c20 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java @@ -11,7 +11,7 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.IndexID3; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import java.util.ArrayList; import java.util.Arrays; @@ -150,7 +150,7 @@ public class ArtistRepository { if(response.body().getSubsonicResponse().getArtists() != null && response.body().getSubsonicResponse().getArtists().getIndices() != null) { for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) { - if(index != null && index.getArtists() != null) { + if(index.getArtists() != null) { artists.addAll(index.getArtists()); } } @@ -289,7 +289,7 @@ public class ArtistRepository { public MutableLiveData> getInstantMix(ArtistID3 artist, int count) { // Delegate to the centralized SongRepository - return new SongRepository().getInstantMix(artist.getId(), Constants.SEEDTYPE.ARTIST, count); + return new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count); } public MutableLiveData> getRandomSong(ArtistID3 artist, int count) { From c1b2ec09a4a4edac824cafcda5e109286c12b9f5 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 17:52:29 -0800 Subject: [PATCH 11/25] wip: radio working from artist page --- .../tempo/ui/fragment/ArtistPageFragment.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java index 9fbce6dc..9f904232 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java @@ -9,7 +9,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.NonNull; @@ -32,6 +31,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter; import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter; @@ -203,22 +203,27 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { } }); } + private void initPlayButtons() { - bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), songs -> { - if (!songs.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - activity.setBottomSheetInPeek(true); - } else { - Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show(); + bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null && !songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + artistPageViewModel.getArtistShuffleList().removeObserver(this); + } } })); - bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> { - if (songs != null && !songs.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - activity.setBottomSheetInPeek(true); - } else { - Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_radio), Toast.LENGTH_SHORT).show(); + bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null && !songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + artistPageViewModel.getArtistInstantMix().removeObserver(this); + } } })); } From 8140e80d611bac8caacc3a8c826d46b771844945 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 19:05:14 -0800 Subject: [PATCH 12/25] wip: artist logic squared away, seems to be working as expected, mostly. still need more testing --- .../tempo/ui/fragment/ArtistPageFragment.java | 5 +-- .../ArtistBottomSheetDialog.java | 35 +++++++++---------- .../tempo/viewmodel/ArtistPageViewModel.java | 1 - 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java index 9f904232..0c850038 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java @@ -14,6 +14,7 @@ import android.widget.ToggleButton; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaBrowser; @@ -205,7 +206,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { } private void initPlayButtons() { - bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { + bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), new Observer>() { @Override public void onChanged(List songs) { if (songs != null && !songs.isEmpty()) { @@ -216,7 +217,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { } })); - bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { + bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), new Observer>() { @Override public void onChanged(List songs) { if (songs != null && !songs.isEmpty()) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index 9ec9b549..e890c10e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -7,10 +7,10 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.Nullable; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaBrowser; @@ -22,6 +22,7 @@ import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.MusicUtil; @@ -29,6 +30,8 @@ import com.cappielloantonio.tempo.viewmodel.ArtistBottomSheetViewModel; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.common.util.concurrent.ListenableFuture; +import java.util.List; + @UnstableApi public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { private static final String TAG = "AlbumBottomSheetDialog"; @@ -87,20 +90,20 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { ArtistRepository artistRepository = new ArtistRepository(); - - artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> { - // navidrome may return null for this - if (songs == null) - return; - MusicUtil.ratingFilter(songs); - - if (!songs.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + Observer> observer = new Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null && !songs.isEmpty() && isAdded()) { + MusicUtil.ratingFilter(songs); + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + artistRepository.getInstantMix(artist, 20).removeObserver(this); + dismissBottomSheet(); + } } + }; - dismissBottomSheet(); - }); + artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), observer); }); TextView playRandom = view.findViewById(R.id.play_random_text_view); @@ -108,16 +111,10 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement ArtistRepository artistRepository = new ArtistRepository(); artistRepository.getRandomSong(artist, 50).observe(getViewLifecycleOwner(), songs -> { MusicUtil.ratingFilter(songs); - if (!songs.isEmpty()) { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); ((MainActivity) requireActivity()).setBottomSheetInPeek(true); - - dismissBottomSheet(); - } else { - Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show(); } - dismissBottomSheet(); }); }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java index 871565d0..ab6cc609 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java @@ -128,7 +128,6 @@ public class ArtistPageViewModel extends AndroidViewModel { MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).collect(Collectors.toList()) ); - } else { } } }); From 10673a49d48bbe7a8f8e7957cfefb6d13f9426a6 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 28 Dec 2025 08:20:58 -0800 Subject: [PATCH 13/25] chore: bump version, bringing in dev changes to test --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 43b139e3..036ceed6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { targetSdk 35 versionCode 11 - versionName '4.6.1' + versionName '4.6.2' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { From 8c5390bfef82dc65341e2f816bfea3bffdbeb556 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Mon, 29 Dec 2025 16:36:42 -0800 Subject: [PATCH 14/25] wip: more instant mix refactor to be able to accumulate tracks --- .../tempo/repository/SongRepository.java | 100 ++++++++++++++---- 1 file changed, 78 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java index 5d79e283..a3b14031 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -87,7 +87,45 @@ public class SongRepository { * Overloaded method used by other Repositories */ public void getInstantMix(String id, SeedType type, int count, MediaCallbackInternal callback) { - performSmartMix(id, type, count, callback); + new MediaCallbackAccumulator(callback, count).start(id, type); + } + + private class MediaCallbackAccumulator { + private final MediaCallbackInternal originalCallback; + private final int targetCount; + private final List accumulatedSongs = new ArrayList<>(); + private boolean isComplete = false; + + MediaCallbackAccumulator(MediaCallbackInternal callback, int count) { + this.originalCallback = callback; + this.targetCount = count; + } + + void start(String id, SeedType type) { + performSmartMix(id, type, targetCount, this::onBatchReceived); + } + + private void onBatchReceived(List batch) { + if (isComplete || batch == null || batch.isEmpty()) { + return; + } + + int added = 0; + for (Child s : batch) { + if (!accumulatedSongs.contains(s) && accumulatedSongs.size() < targetCount) { + accumulatedSongs.add(s); + added++; + } + } + + if (added > 0) { + originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs)); + if (accumulatedSongs.size() >= targetCount) { + isComplete = true; + android.util.Log.d("SongRepository", "Reached target of " + targetCount + " songs"); + } + } + } } private void performSmartMix(final String id, final SeedType type, final int count, final MediaCallbackInternal callback) { @@ -111,8 +149,14 @@ public class SongRepository { if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { List albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); if (albumSongs != null && !albumSongs.isEmpty()) { - callback.onSongsAvailable(new ArrayList<>(albumSongs)); - fetchSimilarByArtist(albumSongs.get(0).getArtistId(), count, callback); + List limitedAlbumSongs = albumSongs.subList(0, Math.min(count, albumSongs.size())); + callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs)); + + // If we need more, get similar songs + int remaining = count - limitedAlbumSongs.size(); + if (remaining > 0) { + fetchSimilarByArtist(albumSongs.get(0).getArtistId(), remaining, callback); + } return; } } @@ -132,7 +176,10 @@ public class SongRepository { Child song = response.body().getSubsonicResponse().getSong(); if (song != null) { callback.onSongsAvailable(Collections.singletonList(song)); - fetchSimilarOnly(trackId, count, callback); + int remaining = count - 1; + if (remaining > 0) { + fetchSimilarOnly(trackId, remaining, callback); + } return; } } @@ -144,23 +191,23 @@ public class SongRepository { }); } - private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { - App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = extractSongs(response, "similarSongs"); - if (!songs.isEmpty()) { - callback.onSongsAvailable(songs); - } else { - fillWithRandom(count, callback); - } - } - @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { +private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = extractSongs(response, "similarSongs"); + if (!songs.isEmpty()) { + List limitedSongs = songs.subList(0, Math.min(count, songs.size())); + callback.onSongsAvailable(limitedSongs); + } else { fillWithRandom(count, callback); } - }); - } - + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } + }); +} private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) { App.getSubsonicClientInstance(false) .getBrowsingClient() @@ -170,7 +217,8 @@ public class SongRepository { public void onResponse(@NonNull Call call, @NonNull Response response) { List similar = extractSongs(response, "similarSongs2"); if (!similar.isEmpty()) { - callback.onSongsAvailable(similar); + List limitedSimilar = similar.subList(0, Math.min(count, similar.size())); + callback.onSongsAvailable(limitedSimilar); } else { fillWithRandom(count, callback); } @@ -189,9 +237,17 @@ public class SongRepository { .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - callback.onSongsAvailable(extractSongs(response, "randomSongs")); + List random = extractSongs(response, "randomSongs"); + if (!random.isEmpty()) { + List limitedRandom = random.subList(0, Math.min(target, random.size())); + callback.onSongsAvailable(limitedRandom); + } else { + callback.onSongsAvailable(new ArrayList<>()); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + callback.onSongsAvailable(new ArrayList<>()); } - @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} }); } From f39891dd2cfa5c883ce78f86fa83bf780bfffc2a Mon Sep 17 00:00:00 2001 From: eddyizm Date: Mon, 29 Dec 2025 16:37:20 -0800 Subject: [PATCH 15/25] wip: added logging to media manager to track down bug in bottom sheet dialogs --- .../tempo/service/MediaManager.java | 22 ++++++- .../AlbumBottomSheetDialog.java | 58 ++++++++++++------- .../ArtistBottomSheetDialog.java | 48 +++++++++++---- 3 files changed, 91 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java index f3139186..12ae3e85 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -184,41 +184,57 @@ public class MediaManager { @OptIn(markerClass = UnstableApi.class) public static void startQueue(ListenableFuture mediaBrowserListenableFuture, List media, int startIndex) { if (mediaBrowserListenableFuture != null) { + Log.d(TAG, "startQueue called with " + (media != null ? media.size() : 0) + " songs"); + mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { + Log.d(TAG, "MediaBrowser future is done"); final MediaBrowser browser = mediaBrowserListenableFuture.get(); + Log.d(TAG, "Got MediaBrowser, connected: " + browser.isConnected()); + final List items = MappingUtil.mapMediaItems(media); + Log.d(TAG, "Mapped " + items.size() + " media items"); + new Handler(Looper.getMainLooper()).post(() -> { + Log.d(TAG, "Setting " + items.size() + " media items at index " + startIndex); justStarted.set(true); browser.setMediaItems(items, startIndex, 0); browser.prepare(); + Log.d(TAG, "setMediaItems and prepare called"); Player.Listener timelineListener = new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { + Log.d(TAG, "onTimelineChanged: itemCount=" + browser.getMediaItemCount() + ", reason=" + reason); + int itemCount = browser.getMediaItemCount(); if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) { + Log.d(TAG, "Seeking to " + startIndex + " and playing"); browser.seekTo(startIndex, 0); browser.play(); browser.removeListener(this); + Log.d(TAG, "Playback started"); + } else { + Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex); } } }; + + Log.d(TAG, "Adding timeline listener"); browser.addListener(timelineListener); }); backgroundExecutor.execute(() -> { + Log.d(TAG, "Background: enqueuing to database"); enqueueDatabase(media, true, 0); }); } } catch (ExecutionException | InterruptedException e) { - Log.e(TAG, "Error executing startQueue logic: " + e.getMessage(), e); + Log.e(TAG, "Error in startQueue: " + e.getMessage(), e); } }, MoreExecutors.directExecutor()); } - - } public static void startQueue(ListenableFuture mediaBrowserListenableFuture, Child media) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 5db3add0..072fa391 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -116,33 +116,47 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements favoriteToggle.setOnClickListener(v -> albumBottomSheetViewModel.setFavorite(requireContext())); TextView playRadio = view.findViewById(R.id.play_radio_text_view); - playRadio.setOnClickListener(v -> { - AlbumRepository albumRepository = new AlbumRepository(); - albumRepository.getInstantMix(album, 20, new MediaCallback() { - @Override - public void onError(Exception exception) { - exception.printStackTrace(); - } - - @Override - public void onLoadMedia(List media) { - if (!isAdded() || getActivity() == null) { - return; + playRadio.setOnClickListener(v -> { + AlbumRepository albumRepository = new AlbumRepository(); + albumRepository.getInstantMix(album, 20, new MediaCallback() { + @Override + public void onError(Exception exception) { + exception.printStackTrace(); } - MusicUtil.ratingFilter((ArrayList) media); - - if (!media.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); - if (getActivity() instanceof MainActivity) { - ((MainActivity) getActivity()).setBottomSheetInPeek(true); + @Override + public void onLoadMedia(List media) { + if (!isAdded() || getActivity() == null) { + return; } - } - dismissBottomSheet(); - } + MusicUtil.ratingFilter((ArrayList) media); + + if (!media.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).setBottomSheetInPeek(true); + } + } + + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + // Ignore + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + } + }); }); - }); + TextView playRandom = view.findViewById(R.id.play_random_text_view); playRandom.setOnClickListener(v -> { AlbumRepository albumRepository = new AlbumRepository(); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index e890c10e..2f48e4dc 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -2,11 +2,13 @@ package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog; import android.content.ComponentName; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.Nullable; @@ -89,21 +91,43 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { + Log.d(TAG, "Artist instant mix clicked"); + ArtistRepository artistRepository = new ArtistRepository(); - Observer> observer = new Observer>() { - @Override - public void onChanged(List songs) { - if (songs != null && !songs.isEmpty() && isAdded()) { - MusicUtil.ratingFilter(songs); - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - ((MainActivity) requireActivity()).setBottomSheetInPeek(true); - artistRepository.getInstantMix(artist, 20).removeObserver(this); + artistRepository.getInstantMix(artist, 20) + .observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null && !songs.isEmpty()) { + Log.d(TAG, "Starting queue with " + songs.size() + " songs"); + MusicUtil.ratingFilter(songs); + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + artistRepository.getInstantMix(artist, 20) + .removeObserver(this); + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + // Ignore + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + } else { + // No songs at all - all attempts failed + Toast.makeText(requireContext(), + "Could not load songs. Please check your connection.", + Toast.LENGTH_SHORT).show(); dismissBottomSheet(); } - } - }; - - artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), observer); + } + }); }); TextView playRandom = view.findViewById(R.id.play_random_text_view); From a2401302edc0687270f69e42d640ac7a53bece63 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Tue, 30 Dec 2025 21:46:07 -0800 Subject: [PATCH 16/25] wip: beta build --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 036ceed6..d1418559 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { targetSdk 35 versionCode 11 - versionName '4.6.2' + versionName '4.6.2.BETA' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { From 193447d07e5ebd84764bb3e9a0cd6a39faaf0b08 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Wed, 31 Dec 2025 08:20:03 -0800 Subject: [PATCH 17/25] fix: used set to address duplicates and removed toast that was firing to early --- .../tempo/repository/SongRepository.java | 4 ++++ .../bottomsheetdialog/ArtistBottomSheetDialog.java | 10 +--------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java index a3b14031..0c82e022 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -11,8 +11,10 @@ import com.cappielloantonio.tempo.util.Constants.SeedType; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import retrofit2.Call; import retrofit2.Callback; @@ -94,6 +96,7 @@ public class SongRepository { private final MediaCallbackInternal originalCallback; private final int targetCount; private final List accumulatedSongs = new ArrayList<>(); + private final Set trackIds = new HashSet<>(); private boolean isComplete = false; MediaCallbackAccumulator(MediaCallbackInternal callback, int count) { @@ -113,6 +116,7 @@ public class SongRepository { int added = 0; for (Child s : batch) { if (!accumulatedSongs.contains(s) && accumulatedSongs.size() < targetCount) { + trackIds.add(s.getId()); accumulatedSongs.add(s); added++; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index 2f48e4dc..b6e88c1a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -8,11 +8,9 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.Nullable; -import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaBrowser; @@ -119,13 +117,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement } view.postDelayed(() -> dismissBottomSheet(), 200); }, 300); - } else { - // No songs at all - all attempts failed - Toast.makeText(requireContext(), - "Could not load songs. Please check your connection.", - Toast.LENGTH_SHORT).show(); - dismissBottomSheet(); - } + } } }); }); From d04ed8d430a819feca4832e3e465ad41870de815 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Thu, 1 Jan 2026 11:50:48 -0800 Subject: [PATCH 18/25] fix: address duplicate track bug, wrong order in queue, and updated album instant mix --- .../tempo/repository/SongRepository.java | 101 ++++++++++-------- .../tempo/service/MediaManager.java | 13 +-- .../ui/fragment/HomeTabMusicFragment.java | 23 ++-- .../AlbumBottomSheetDialog.java | 34 +++--- .../ArtistBottomSheetDialog.java | 2 +- .../SongBottomSheetDialog.java | 37 +++++-- 6 files changed, 120 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java index 0c82e022..d457c44a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -1,5 +1,7 @@ package com.cappielloantonio.tempo.repository; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; @@ -22,6 +24,7 @@ import retrofit2.Response; public class SongRepository { + private static final String TAG = "SongRepository"; public interface MediaCallbackInternal { void onSongsAvailable(List songs); } @@ -126,7 +129,6 @@ public class SongRepository { originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs)); if (accumulatedSongs.size() >= targetCount) { isComplete = true; - android.util.Log.d("SongRepository", "Reached target of " + targetCount + " songs"); } } } @@ -150,28 +152,61 @@ public class SongRepository { App.getSubsonicClientInstance(false).getBrowsingClient().getAlbum(albumId).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { + if (response.isSuccessful() && response.body() != null && + response.body().getSubsonicResponse().getAlbum() != null) { List albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); if (albumSongs != null && !albumSongs.isEmpty()) { - List limitedAlbumSongs = albumSongs.subList(0, Math.min(count, albumSongs.size())); + int fromAlbum = Math.min(count, albumSongs.size()); + List limitedAlbumSongs = albumSongs.subList(0, fromAlbum); callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs)); - - // If we need more, get similar songs - int remaining = count - limitedAlbumSongs.size(); - if (remaining > 0) { + + int remaining = count - fromAlbum; + if (remaining > 0 && albumSongs.get(0).getArtistId() != null) { fetchSimilarByArtist(albumSongs.get(0).getArtistId(), remaining, callback); + } else if (remaining > 0) { + Log.d(TAG, "No artistId available, skipping similar artist fetch"); } return; } } + + Log.d(TAG, "Album fetch failed or empty, calling fillWithRandom"); fillWithRandom(count, callback); } - @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.d(TAG, "Album fetch failed: " + t.getMessage()); fillWithRandom(count, callback); } }); } + private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSimilarSongs2(artistId, count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List similar = extractSongs(response, "similarSongs2"); + if (!similar.isEmpty()) { + List limitedSimilar = similar.subList(0, Math.min(count, similar.size())); + callback.onSongsAvailable(limitedSimilar); + } else { + Log.d(TAG, "No similar songs, calling fillWithRandom"); + fillWithRandom(count, callback); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.d(TAG, "getSimilarSongs2 failed: " + t.getMessage()); + fillWithRandom(count, callback); + } + }); + } + private void fetchSingleTrackThenSimilar(String trackId, int count, MediaCallbackInternal callback) { App.getSubsonicClientInstance(false).getBrowsingClient().getSong(trackId).enqueue(new Callback() { @Override @@ -195,45 +230,25 @@ public class SongRepository { }); } -private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { - App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = extractSongs(response, "similarSongs"); - if (!songs.isEmpty()) { - List limitedSongs = songs.subList(0, Math.min(count, songs.size())); - callback.onSongsAvailable(limitedSongs); - } else { + private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = extractSongs(response, "similarSongs"); + if (!songs.isEmpty()) { + List limitedSongs = songs.subList(0, Math.min(count, songs.size())); + callback.onSongsAvailable(limitedSongs); + } else { + fillWithRandom(count, callback); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { fillWithRandom(count, callback); } - } - @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - fillWithRandom(count, callback); - } - }); -} - private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) { - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSimilarSongs2(artistId, count) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List similar = extractSongs(response, "similarSongs2"); - if (!similar.isEmpty()) { - List limitedSimilar = similar.subList(0, Math.min(count, similar.size())); - callback.onSongsAvailable(limitedSimilar); - } else { - fillWithRandom(count, callback); - } - } - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - fillWithRandom(count, callback); - } - }); + }); } + private void fillWithRandom(int target, final MediaCallbackInternal callback) { App.getSubsonicClientInstance(false) .getAlbumSongListClient() diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java index 12ae3e85..ab591104 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -184,44 +184,33 @@ public class MediaManager { @OptIn(markerClass = UnstableApi.class) public static void startQueue(ListenableFuture mediaBrowserListenableFuture, List media, int startIndex) { if (mediaBrowserListenableFuture != null) { - Log.d(TAG, "startQueue called with " + (media != null ? media.size() : 0) + " songs"); - + mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { - Log.d(TAG, "MediaBrowser future is done"); final MediaBrowser browser = mediaBrowserListenableFuture.get(); - Log.d(TAG, "Got MediaBrowser, connected: " + browser.isConnected()); - final List items = MappingUtil.mapMediaItems(media); - Log.d(TAG, "Mapped " + items.size() + " media items"); new Handler(Looper.getMainLooper()).post(() -> { - Log.d(TAG, "Setting " + items.size() + " media items at index " + startIndex); justStarted.set(true); browser.setMediaItems(items, startIndex, 0); browser.prepare(); - Log.d(TAG, "setMediaItems and prepare called"); Player.Listener timelineListener = new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - Log.d(TAG, "onTimelineChanged: itemCount=" + browser.getMediaItemCount() + ", reason=" + reason); int itemCount = browser.getMediaItemCount(); if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) { - Log.d(TAG, "Seeking to " + startIndex + " and playing"); browser.seekTo(startIndex, 0); browser.play(); browser.removeListener(this); - Log.d(TAG, "Playback started"); } else { Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex); } } }; - Log.d(TAG, "Adding timeline listener"); browser.addListener(timelineListener); }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java index 191c520d..d7f739ae 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java @@ -5,6 +5,8 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -1253,20 +1255,25 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { MediaBrowser.releaseFuture(mediaBrowserListenableFuture); } - @Override public void onMediaClick(Bundle bundle) { if (bundle.containsKey(Constants.MEDIA_MIX)) { - MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelable(Constants.TRACK_OBJECT)); + Child track = bundle.getParcelable(Constants.TRACK_OBJECT); activity.setBottomSheetInPeek(true); if (mediaBrowserListenableFuture != null) { - homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.TRACK_OBJECT)).observe(getViewLifecycleOwner(), songs -> { - MusicUtil.ratingFilter(songs); + final boolean[] playbackStarted = {false}; - if (songs != null && !songs.isEmpty()) { - MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); - } - }); + homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), track) + .observe(getViewLifecycleOwner(), songs -> { + if (playbackStarted[0] || songs == null || songs.isEmpty()) return; + + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (playbackStarted[0]) return; + + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + playbackStarted[0] = true; + }, 300); + }); } } else if (bundle.containsKey(Constants.MEDIA_CHRONOLOGY)) { List media = bundle.getParcelableArrayList(Constants.TRACKS_OBJECT); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 072fa391..f1e4c89a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -5,6 +5,8 @@ import android.content.ClipboardManager; import android.content.ComponentName; import android.content.Context; import android.os.Bundle; +import android.os.Handler; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -61,6 +63,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements private List currentAlbumMediaItems = Collections.emptyList(); private ListenableFuture mediaBrowserListenableFuture; + private static final String TAG = "AlbumBottomSheetDialog"; @Nullable @Override @@ -116,12 +119,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements favoriteToggle.setOnClickListener(v -> albumBottomSheetViewModel.setFavorite(requireContext())); TextView playRadio = view.findViewById(R.id.play_radio_text_view); - playRadio.setOnClickListener(v -> { - AlbumRepository albumRepository = new AlbumRepository(); - albumRepository.getInstantMix(album, 20, new MediaCallback() { + playRadio.setOnClickListener(v -> { + new AlbumRepository().getInstantMix(album, 20, new MediaCallback() { @Override public void onError(Exception exception) { - exception.printStackTrace(); + Log.e(TAG, "Error: " + exception.getMessage()); } @Override @@ -140,19 +142,19 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements } view.postDelayed(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - if (browser != null && browser.isPlaying()) { - dismissBottomSheet(); - return; - } - } - } catch (Exception e) { - // Ignore + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; } - view.postDelayed(() -> dismissBottomSheet(), 200); - }, 300); + } + } catch (Exception e) { + Log.e(TAG, "Error checking playback: " + e.getMessage()); + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); } }); }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index b6e88c1a..be69b76a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -89,7 +89,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { - Log.d(TAG, "Artist instant mix clicked"); + Log.d(TAG, "Artist instant mix clicked"); ArtistRepository artistRepository = new ArtistRepository(); artistRepository.getInstantMix(artist, 20) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java index 39ba4394..271f00cc 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java @@ -5,6 +5,8 @@ import android.content.ClipboardManager; import android.content.ComponentName; import android.content.Context; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -143,21 +145,36 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { - MediaManager.startQueue(mediaBrowserListenableFuture, song); ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + final boolean[] playbackStarted = {false}; + songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> { - MusicUtil.ratingFilter(songs); + if (playbackStarted[0] || songs == null || songs.isEmpty()) return; - if (songs == null) { - dismissBottomSheet(); - return; - } + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (playbackStarted[0]) return; - if (!songs.isEmpty()) { - MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); - dismissBottomSheet(); - } + MusicUtil.ratingFilter(songs); + + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + playbackStarted[0] = true; + + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + // Ignore + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + }, 300); }); }); From 586a1a160e4a658a3f7d71a965d2fd10a2e6978c Mon Sep 17 00:00:00 2001 From: Age Bosma Date: Sat, 3 Jan 2026 15:53:37 +0100 Subject: [PATCH 19/25] Clarify Android Auto enablement --- USAGE.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/USAGE.md b/USAGE.md index d3a946db..4b60af75 100644 --- a/USAGE.md +++ b/USAGE.md @@ -160,7 +160,23 @@ If your server supports it - add a internet radio station feed ## Android Auto ### Enabling on your head unit -- You have to enable Android Auto developer options, which are different from actual Android dev options. Then you have to enable "Unknown sources" in Android Auto, otherwise the app won't appear as it isn't downloaded from Play Store. (screenshots needed) +To allow the Tempus app on your car's head unit, "Unknown sources" needs to be enabled in the Android Auto "Developer settings". This is because Tempus isn't installed through Play Store. Note that the Android Auto developer settings are different from the global Android "Developer options". +1. Switch to developer mode in the Android Auto settings by tapping ten times on the "Version" item at the bottom, followed by giving your permission. +

+ 1a + 1b + 1c +

+ +2. Go to the "Developer settings" by the menu at the top right. +

+ 2 +

+ +3. Scroll down to the bottom and check "Unknown sources". +

+ 3 +

### Server Settings From 05785979e3a31dd7d81d6767f54d40cdd29e0eb4 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 3 Jan 2026 08:17:53 -0800 Subject: [PATCH 20/25] fix: cleans up duplicates --- .../tempo/repository/SongRepository.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java index d457c44a..25990236 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -117,19 +117,17 @@ public class SongRepository { } int added = 0; - for (Child s : batch) { - if (!accumulatedSongs.contains(s) && accumulatedSongs.size() < targetCount) { - trackIds.add(s.getId()); - accumulatedSongs.add(s); + for (Child song : batch) { + if (!trackIds.contains(song.getId()) && accumulatedSongs.size() < targetCount) { + trackIds.add(song.getId()); + accumulatedSongs.add(song); added++; } } - if (added > 0) { + if (accumulatedSongs.size() >= targetCount) { originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs)); - if (accumulatedSongs.size() >= targetCount) { - isComplete = true; - } + isComplete = true; } } } @@ -194,14 +192,12 @@ public class SongRepository { List limitedSimilar = similar.subList(0, Math.min(count, similar.size())); callback.onSongsAvailable(limitedSimilar); } else { - Log.d(TAG, "No similar songs, calling fillWithRandom"); fillWithRandom(count, callback); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - Log.d(TAG, "getSimilarSongs2 failed: " + t.getMessage()); fillWithRandom(count, callback); } }); From 99c31f431857bdb846d8f005022717d79d973654 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 3 Jan 2026 08:18:29 -0800 Subject: [PATCH 21/25] wip: bumps debounce time --- .../ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index f1e4c89a..098d74a6 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -154,7 +154,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements Log.e(TAG, "Error checking playback: " + e.getMessage()); } view.postDelayed(() -> dismissBottomSheet(), 200); - }, 300); + }, 1200); } }); }); From a2801f316834075e01d9de5e20bc7c26ce83ea66 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 4 Jan 2026 07:53:07 -0800 Subject: [PATCH 22/25] fix: reduced debounce, added toast --- .../fragment/bottomsheetdialog/AlbumBottomSheetDialog.java | 5 +++-- .../fragment/bottomsheetdialog/ArtistBottomSheetDialog.java | 5 +++-- app/src/main/res/values/strings.xml | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 098d74a6..563b1ab3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -120,6 +120,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { + Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); new AlbumRepository().getInstantMix(album, 20, new MediaCallback() { @Override public void onError(Exception exception) { @@ -153,8 +154,8 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements } catch (Exception e) { Log.e(TAG, "Error checking playback: " + e.getMessage()); } - view.postDelayed(() -> dismissBottomSheet(), 200); - }, 1200); + view.postDelayed(() -> dismissBottomSheet(), 300); + }, 300); } }); }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index be69b76a..4d50565a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -8,6 +8,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.Nullable; @@ -90,7 +91,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { Log.d(TAG, "Artist instant mix clicked"); - + Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_LONG).show(); ArtistRepository artistRepository = new ArtistRepository(); artistRepository.getInstantMix(artist, 20) .observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { @@ -115,7 +116,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement } catch (Exception e) { // Ignore } - view.postDelayed(() -> dismissBottomSheet(), 200); + view.postDelayed(() -> dismissBottomSheet(), 300); }, 300); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee8eebe5..75573707 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ Ignore Don\'t ask again Disable + Generating instant mix... Cancel Enable data saver OK From 993374e56c314e0f5eb49f43e93f0d58b75fe30d Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 4 Jan 2026 09:27:53 -0800 Subject: [PATCH 23/25] fix: adde a scheduled delay to allow callbacks to succeed --- .../tempo/repository/ArtistRepository.java | 13 +++ .../AlbumBottomSheetDialog.java | 55 +++++++---- .../ArtistBottomSheetDialog.java | 96 +++++++++++++------ .../viewmodel/AlbumBottomSheetViewModel.java | 4 +- 4 files changed, 122 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java index 7cd69c20..cc47e676 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData; import android.util.Log; import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.interfaces.MediaCallback; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; @@ -292,6 +293,18 @@ public class ArtistRepository { return new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count); } + public void getInstantMix(ArtistID3 artist, int count, final MediaCallback callback) { + // Delegate to the centralized SongRepository + new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count, songs -> { + if (songs != null && !songs.isEmpty()) { + callback.onLoadMedia(songs); + } else { + callback.onLoadMedia(Collections.emptyList()); + } + }); + } + + public MutableLiveData> getRandomSong(ArtistID3 artist, int count) { MutableLiveData> randomSongs = new MutableLiveData<>(); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 563b1ab3..06db820b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -5,7 +5,6 @@ import android.content.ClipboardManager; import android.content.ComponentName; import android.content.Context; import android.os.Bundle; -import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -62,6 +61,9 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements private List currentAlbumTracks = Collections.emptyList(); private List currentAlbumMediaItems = Collections.emptyList(); + private boolean playbackStarted = false; + private boolean dismissalScheduled = false; + private ListenableFuture mediaBrowserListenableFuture; private static final String TAG = "AlbumBottomSheetDialog"; @@ -120,11 +122,16 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { + playbackStarted = false; + dismissalScheduled = false; Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); new AlbumRepository().getInstantMix(album, 20, new MediaCallback() { @Override public void onError(Exception exception) { Log.e(TAG, "Error: " + exception.getMessage()); + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } } @Override @@ -136,30 +143,26 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements MusicUtil.ratingFilter((ArrayList) media); if (!media.isEmpty()) { + boolean isFirstBatch = !playbackStarted; MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + playbackStarted = true; + if (getActivity() instanceof MainActivity) { ((MainActivity) getActivity()).setBottomSheetInPeek(true); } - } - - view.postDelayed(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - if (browser != null && browser.isPlaying()) { - dismissBottomSheet(); - return; - } - } - } catch (Exception e) { - Log.e(TAG, "Error checking playback: " + e.getMessage()); + if (isFirstBatch && !dismissalScheduled) { + scheduleDelayedDismissal(v); } - view.postDelayed(() -> dismissBottomSheet(), 300); - }, 300); + } else { + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } } }); }); + TextView playRandom = view.findViewById(R.id.play_random_text_view); playRandom.setOnClickListener(v -> { AlbumRepository albumRepository = new AlbumRepository(); @@ -308,4 +311,24 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements private void refreshShares() { homeViewModel.refreshShares(requireActivity()); } + + private void scheduleDelayedDismissal(View view) { + if (dismissalScheduled) return; + dismissalScheduled = true; + + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + Log.e(TAG, "Error checking playback: " + e.getMessage()); + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index 4d50565a..91b2cf90 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -19,6 +19,7 @@ import androidx.media3.session.SessionToken; import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.MediaCallback; import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; @@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.viewmodel.ArtistBottomSheetViewModel; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; import java.util.List; @UnstableApi @@ -42,6 +44,9 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement private ListenableFuture mediaBrowserListenableFuture; + private boolean playbackStarted = false; + private boolean dismissalScheduled = false; + @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -91,36 +96,48 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { Log.d(TAG, "Artist instant mix clicked"); - Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_LONG).show(); - ArtistRepository artistRepository = new ArtistRepository(); - artistRepository.getInstantMix(artist, 20) - .observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { - @Override - public void onChanged(List songs) { - if (songs != null && !songs.isEmpty()) { - Log.d(TAG, "Starting queue with " + songs.size() + " songs"); - MusicUtil.ratingFilter(songs); - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - ((MainActivity) requireActivity()).setBottomSheetInPeek(true); - artistRepository.getInstantMix(artist, 20) - .removeObserver(this); - view.postDelayed(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - if (browser != null && browser.isPlaying()) { - dismissBottomSheet(); - return; - } - } - } catch (Exception e) { - // Ignore - } - view.postDelayed(() -> dismissBottomSheet(), 300); - }, 300); + Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); + playbackStarted = false; + dismissalScheduled = false; + + new ArtistRepository().getInstantMix(artist, 20, new MediaCallback() { + @Override + public void onError(Exception exception) { + Log.e(TAG, "Error: " + exception.getMessage()); + + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } + + @Override + public void onLoadMedia(List media) { + if (!isAdded() || getActivity() == null) { + return; + } + + Log.d(TAG, "Received " + media.size() + " songs for artist"); + + MusicUtil.ratingFilter((ArrayList) media); + + if (!media.isEmpty()) { + boolean isFirstBatch = !playbackStarted; + MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + playbackStarted = true; + + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).setBottomSheetInPeek(true); + } + if (isFirstBatch && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } else { + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); } } - }); + } + }); }); TextView playRandom = view.findViewById(R.id.play_random_text_view); @@ -153,4 +170,25 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement private void releaseMediaBrowser() { MediaBrowser.releaseFuture(mediaBrowserListenableFuture); } -} \ No newline at end of file + + private void scheduleDelayedDismissal(View view) { + if (dismissalScheduled) return; + dismissalScheduled = true; + + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + Log.e(TAG, "Error checking playback: " + e.getMessage()); + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + } + +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java index da1ec831..07b17a48 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java @@ -4,10 +4,12 @@ import android.app.Application; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.OptIn; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; +import androidx.media3.common.util.UnstableApi; import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.interfaces.StarCallback; @@ -33,7 +35,6 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel { private final ArtistRepository artistRepository; private final FavoriteRepository favoriteRepository; private final SharingRepository sharingRepository; - private AlbumID3 album; public AlbumBottomSheetViewModel(@NonNull Application application) { @@ -116,6 +117,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel { MutableLiveData> tracksLiveData = albumRepository.getAlbumTracks(album.getId()); tracksLiveData.observeForever(new Observer>() { + @OptIn(markerClass = UnstableApi.class) @Override public void onChanged(List songs) { if (songs != null && !songs.isEmpty()) { From 6110a9c8e7ef9fdca17f0fdac439db6b92f4e5c1 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 4 Jan 2026 10:05:16 -0800 Subject: [PATCH 24/25] fix: added a timeout for the callbacks to dismiss dialog and notify the user --- app/build.gradle | 2 +- .../AlbumBottomSheetDialog.java | 30 +++++++++++++++++++ .../ArtistBottomSheetDialog.java | 29 +++++++++++++++++- app/src/main/res/values/strings.xml | 1 + 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d1418559..036ceed6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { targetSdk 35 versionCode 11 - versionName '4.6.2.BETA' + versionName '4.6.2' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 06db820b..ce4a8e08 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -125,10 +125,29 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements playbackStarted = false; dismissalScheduled = false; Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); + final Runnable failsafeTimeout = () -> { + if (!playbackStarted && !dismissalScheduled) { + Log.w(TAG, "No response received within 3 seconds"); + if (isAdded() && getActivity() != null) { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); + dismissBottomSheet(); + } + } + }; + view.postDelayed(failsafeTimeout, 3000); + new AlbumRepository().getInstantMix(album, 20, new MediaCallback() { @Override public void onError(Exception exception) { + view.removeCallbacks(failsafeTimeout); Log.e(TAG, "Error: " + exception.getMessage()); + if (isAdded() && getActivity() != null) { + String message = isOffline(exception) ? + "You're offline" : "Network error"; + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } if (!playbackStarted && !dismissalScheduled) { scheduleDelayedDismissal(v); } @@ -136,6 +155,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements @Override public void onLoadMedia(List media) { + view.removeCallbacks(failsafeTimeout); if (!isAdded() || getActivity() == null) { return; } @@ -154,6 +174,9 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements scheduleDelayedDismissal(v); } } else { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); if (!playbackStarted && !dismissalScheduled) { scheduleDelayedDismissal(v); } @@ -331,4 +354,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements view.postDelayed(() -> dismissBottomSheet(), 200); }, 300); } + + private boolean isOffline(Exception exception) { + return exception != null && exception.getMessage() != null && + (exception.getMessage().contains("Network") || + exception.getMessage().contains("timeout") || + exception.getMessage().contains("offline")); + } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index 91b2cf90..3f6f1a5e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -99,12 +99,29 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); playbackStarted = false; dismissalScheduled = false; + final Runnable failsafeTimeout = () -> { + if (!playbackStarted && !dismissalScheduled) { + Log.w(TAG, "No response received within 3 seconds"); + if (isAdded() && getActivity() != null) { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); + dismissBottomSheet(); + } + } + }; + view.postDelayed(failsafeTimeout, 3000); new ArtistRepository().getInstantMix(artist, 20, new MediaCallback() { @Override public void onError(Exception exception) { + view.removeCallbacks(failsafeTimeout); Log.e(TAG, "Error: " + exception.getMessage()); - + if (isAdded() && getActivity() != null) { + String message = isOffline(exception) ? + "You're offline" : "Network error"; + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } if (!playbackStarted && !dismissalScheduled) { scheduleDelayedDismissal(v); } @@ -112,6 +129,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement @Override public void onLoadMedia(List media) { + view.removeCallbacks(failsafeTimeout); if (!isAdded() || getActivity() == null) { return; } @@ -132,6 +150,9 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement scheduleDelayedDismissal(v); } } else { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); if (!playbackStarted && !dismissalScheduled) { scheduleDelayedDismissal(v); } @@ -191,4 +212,10 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement }, 300); } + private boolean isOffline(Exception exception) { + return exception != null && exception.getMessage() != null && + (exception.getMessage().contains("Network") || + exception.getMessage().contains("timeout") || + exception.getMessage().contains("offline")); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 75573707..0ae9be02 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,6 +52,7 @@ Don\'t ask again Disable Generating instant mix... + Could not retrieve tracks from subsonic server. Cancel Enable data saver OK From 431014adc4b4898864e8d0e71f6c4bba64c40a37 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 4 Jan 2026 11:31:53 -0800 Subject: [PATCH 25/25] fix: updated song bottom sheet to match album/artist bottom sheets --- .../SongBottomSheetDialog.java | 115 ++++++++++++++---- .../viewmodel/SongBottomSheetViewModel.java | 12 ++ 2 files changed, 102 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java index 271f00cc..27f15b3d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java @@ -7,6 +7,7 @@ import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -25,6 +26,7 @@ import androidx.navigation.fragment.NavHostFragment; import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.MediaCallback; import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; @@ -52,6 +54,7 @@ import com.cappielloantonio.tempo.util.ExternalAudioWriter; import java.util.ArrayList; import java.util.Collections; +import java.util.List; @UnstableApi public class SongBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { @@ -69,7 +72,11 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements private AssetLinkUtil.AssetLink currentAlbumLink; private AssetLinkUtil.AssetLink currentArtistLink; + private boolean playbackStarted = false; + private boolean dismissalScheduled = false; + private ListenableFuture mediaBrowserListenableFuture; + private static final String TAG = "SongBottomSheetDialog"; @Nullable @Override @@ -145,37 +152,68 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { - ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + playbackStarted = false; + dismissalScheduled = false; + Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); - final boolean[] playbackStarted = {false}; + final Runnable failsafeTimeout = () -> { + if (!playbackStarted && !dismissalScheduled) { + Log.w(TAG, "No response received within 3 seconds"); + if (isAdded() && getActivity() != null) { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); + dismissBottomSheet(); + } + } + }; + view.postDelayed(failsafeTimeout, 3000); + songBottomSheetViewModel.getInstantMix(song, 20, new MediaCallback() { + @Override + public void onError(Exception exception) { + view.removeCallbacks(failsafeTimeout); + Log.e(TAG, "Error: " + exception.getMessage()); + if (isAdded() && getActivity() != null) { + String message = isOffline(exception) ? + "You're offline" : "Network error"; + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } - songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> { - if (playbackStarted[0] || songs == null || songs.isEmpty()) return; + @Override + public void onLoadMedia(List media) { + view.removeCallbacks(failsafeTimeout); + if (!isAdded() || getActivity() == null) { + return; + } - new Handler(Looper.getMainLooper()).postDelayed(() -> { - if (playbackStarted[0]) return; + MusicUtil.ratingFilter((ArrayList) media); - MusicUtil.ratingFilter(songs); - - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - playbackStarted[0] = true; - - view.postDelayed(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - if (browser != null && browser.isPlaying()) { - dismissBottomSheet(); - return; - } - } - } catch (Exception e) { - // Ignore + if (!media.isEmpty()) { + boolean isFirstBatch = !playbackStarted; + MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + playbackStarted = true; + + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).setBottomSheetInPeek(true); } - view.postDelayed(() -> dismissBottomSheet(), 200); - }, 300); - }, 300); + if (isFirstBatch && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } else { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } + } }); + }); TextView playNext = view.findViewById(R.id.play_next_text_view); @@ -414,4 +452,31 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements private void refreshShares() { homeViewModel.refreshShares(requireActivity()); } + + private void scheduleDelayedDismissal(View view) { + if (dismissalScheduled) return; + dismissalScheduled = true; + + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + Log.e(TAG, "Error checking playback: " + e.getMessage()); + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + } + + private boolean isOffline(Exception exception) { + return exception != null && exception.getMessage() != null && + (exception.getMessage().contains("Network") || + exception.getMessage().contains("timeout") || + exception.getMessage().contains("offline")); + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java index efa74e90..665756a7 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java @@ -10,6 +10,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.media3.common.util.UnstableApi; +import com.cappielloantonio.tempo.interfaces.MediaCallback; import com.cappielloantonio.tempo.interfaces.StarCallback; import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.repository.AlbumRepository; @@ -134,6 +135,17 @@ public class SongBottomSheetViewModel extends AndroidViewModel { return instantMix; } + public void getInstantMix(Child media, int count, MediaCallback callback) { + + songRepository.getInstantMix(media.getId(), SeedType.TRACK, count, songs -> { + if (songs != null && !songs.isEmpty()) { + callback.onLoadMedia(songs); + } else { + callback.onLoadMedia(Collections.emptyList()); + } + }); + } + public MutableLiveData shareTrack() { return sharingRepository.createShare(song.getId(), song.getTitle(), null); }