From 5ab68e4a98e780f9635aba071affcdffc187d4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Garc=C3=ADa?= <55400857+jaime-grj@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:28:01 +0200 Subject: [PATCH] feat: Add play/pause button in song lists --- .../tempo/interfaces/MediaSongIdCallback.java | 8 -- .../tempo/service/MediaManager.java | 98 ++++++++++--- .../ui/adapter/PlayerSongQueueAdapter.java | 107 ++++++++++++-- .../ui/adapter/SongHorizontalAdapter.java | 133 ++++++++++++++---- .../tempo/ui/fragment/AlbumPageFragment.java | 39 +++-- .../tempo/ui/fragment/ArtistPageFragment.java | 36 +++-- .../ui/fragment/HomeTabMusicFragment.java | 61 ++++++-- .../ui/fragment/PlayerQueueFragment.java | 39 ++++- .../ui/fragment/PlaylistPageFragment.java | 39 +++-- .../tempo/ui/fragment/SearchFragment.java | 31 +++- .../ui/fragment/SongListPageFragment.java | 35 +++-- .../tempo/viewmodel/PlaybackViewModel.java | 38 +++++ .../main/res/layout/item_horizontal_track.xml | 13 +- .../res/layout/item_player_queue_song.xml | 13 +- 14 files changed, 555 insertions(+), 135 deletions(-) delete mode 100644 app/src/main/java/com/cappielloantonio/tempo/interfaces/MediaSongIdCallback.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaybackViewModel.java diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/MediaSongIdCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/MediaSongIdCallback.java deleted file mode 100644 index de147c58..00000000 --- a/app/src/main/java/com/cappielloantonio/tempo/interfaces/MediaSongIdCallback.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.cappielloantonio.tempo.interfaces; - -import androidx.annotation.Keep; - -@Keep -public interface MediaSongIdCallback { - default void onRecovery(String id) {} -} 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 9c9947ba..538e1dd5 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -2,17 +2,20 @@ package com.cappielloantonio.tempo.service; import android.content.ComponentName; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.OptIn; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.media3.common.MediaItem; +import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaBrowser; import androidx.media3.session.SessionToken; import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.interfaces.MediaIndexCallback; -import com.cappielloantonio.tempo.interfaces.MediaSongIdCallback; import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.repository.ChronologyRepository; import com.cappielloantonio.tempo.repository.QueueRepository; @@ -22,14 +25,88 @@ import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import java.lang.ref.WeakReference; import java.util.List; import java.util.concurrent.ExecutionException; public class MediaManager { private static final String TAG = "MediaManager"; + private static WeakReference attachedBrowserRef = new WeakReference<>(null); + + /** + * Attach a Player.Listener to the MediaBrowser (once per browser instance). + * Safe to call every time you (re)create the MediaBrowser future (e.g. in Fragment.onStart()). + */ + public static void registerPlaybackObserver( + LifecycleOwner lifecycleOwner, + ListenableFuture browserFuture, + PlaybackViewModel playbackViewModel + ) { + if (browserFuture == null) return; + + Futures.addCallback(browserFuture, new FutureCallback() { + @Override + public void onSuccess(MediaBrowser browser) { + MediaBrowser current = attachedBrowserRef.get(); + if (current != browser) { + browser.addListener(new Player.Listener() { + @Override + public void onEvents(@NonNull Player player, @NonNull Player.Events events) { + if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION) + || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED) + || events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) { + + String mediaId = player.getCurrentMediaItem() != null + ? player.getCurrentMediaItem().mediaId + : null; + + boolean playing = player.getPlaybackState() == Player.STATE_READY + && player.getPlayWhenReady(); + + playbackViewModel.update(mediaId, playing); + } + } + }); + + String mediaId = browser.getCurrentMediaItem() != null + ? browser.getCurrentMediaItem().mediaId + : null; + boolean playing = browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady(); + playbackViewModel.update(mediaId, playing); + + attachedBrowserRef = new WeakReference<>(browser); + } else { + String mediaId = browser.getCurrentMediaItem() != null + ? browser.getCurrentMediaItem().mediaId + : null; + boolean playing = browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady(); + playbackViewModel.update(mediaId, playing); + } + } + + @Override + public void onFailure(@NonNull Throwable t) { + // Log or handle if needed + } + }, MoreExecutors.directExecutor()); + } + + /** + * Call this when you truly want to discard the browser (e.g. Activity.onStop()). + * If fragments call it, they should accept that next onStart will recreate a browser & listener. + */ + public static void onBrowserReleased(@Nullable MediaBrowser released) { + MediaBrowser attached = attachedBrowserRef.get(); + if (attached == released) { + attachedBrowserRef.clear(); + } + } public static void reset(ListenableFuture mediaBrowserListenableFuture) { if (mediaBrowserListenableFuture != null) { @@ -293,25 +370,6 @@ public class MediaManager { } } - public static void getCurrentSongId(ListenableFuture mediaBrowserListenableFuture, MediaSongIdCallback callback) { - if (mediaBrowserListenableFuture != null) { - mediaBrowserListenableFuture.addListener(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - MediaItem currentItem = mediaBrowserListenableFuture.get().getCurrentMediaItem(); - if (currentItem != null) { - callback.onRecovery(currentItem.mediaMetadata.extras.getString("id")); - } else { - callback.onRecovery(null); - } - } - } catch (ExecutionException | InterruptedException e) { - e.printStackTrace(); - } - }, MoreExecutors.directExecutor()); - } - } - public static void setLastPlayedTimestamp(MediaItem mediaItem) { if (mediaItem != null) getQueueRepository().setLastPlayedTimestamp(mediaItem.mediaId); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java index c155174b..e6f25d66 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java @@ -1,7 +1,9 @@ package com.cappielloantonio.tempo.ui.adapter; +import android.app.Activity; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -17,24 +19,30 @@ import com.cappielloantonio.tempo.databinding.ItemPlayerQueueSongBinding; import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.interfaces.MediaIndexCallback; -import com.cappielloantonio.tempo.interfaces.MediaSongIdCallback; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.Preferences; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; public class PlayerSongQueueAdapter extends RecyclerView.Adapter { + private static final String TAG = "PlayerSongQueueAdapter"; private final ClickCallback click; private ListenableFuture mediaBrowserListenableFuture; private List songs; + private String currentPlayingId; + private boolean isPlaying; + private List currentPlayingPositions = Collections.emptyList(); + public PlayerSongQueueAdapter(ClickCallback click) { this.click = click; this.songs = Collections.emptyList(); @@ -87,19 +95,6 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter { + mediaBrowserListenableFuture.addListener(() -> { + try { + MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + int pos = holder.getBindingAdapterPosition(); + Child s = songs.get(pos); + if (currentPlayingId != null && currentPlayingId.equals(s.getId())) { + if (isPlaying) { + mediaBrowser.pause(); + } else { + mediaBrowser.play(); + } + } else { + mediaBrowser.seekTo(pos, 0); + mediaBrowser.play(); + } + } catch (Exception e) { + Log.w(TAG, "Error obtaining MediaBrowser", e); + } + }, MoreExecutors.directExecutor()); + + }); + bindPlaybackState(holder, song); + } + + private void bindPlaybackState(@NonNull PlayerSongQueueAdapter.ViewHolder holder, @NonNull Child song) { + boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId()) && isPlaying; + + if (isCurrent) { + holder.item.playPauseButton.setVisibility(View.VISIBLE); + holder.item.playPauseButton.setChecked(true); + holder.item.coverArtOverlay.setVisibility(View.VISIBLE); + } else { + boolean sameIdPaused = currentPlayingId != null && currentPlayingId.equals(song.getId()) && !isPlaying; + if (sameIdPaused) { + holder.item.playPauseButton.setVisibility(View.VISIBLE); + holder.item.playPauseButton.setChecked(false); + holder.item.coverArtOverlay.setVisibility(View.VISIBLE); + } else { + holder.item.playPauseButton.setVisibility(View.GONE); + holder.item.playPauseButton.setChecked(false); + holder.item.coverArtOverlay.setVisibility(View.INVISIBLE); + } + } } public List getItems() { @@ -146,6 +185,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter oldPositions = currentPlayingPositions; + + this.currentPlayingId = mediaId; + this.isPlaying = playing; + + if (Objects.equals(oldId, mediaId) && oldPlaying == playing) { + List newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList(); + if (oldPositions.equals(newPositionsCheck)) { + return; + } + } + + currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList(); + + for (int pos : oldPositions) { + if (pos >= 0 && pos < songs.size()) { + notifyItemChanged(pos, "payload_playback"); + } + } + for (int pos : currentPlayingPositions) { + if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) { + notifyItemChanged(pos, "payload_playback"); + } + } + } + + private List findPositionsById(String id) { + if (id == null) return Collections.emptyList(); + List positions = new ArrayList<>(); + for (int i = 0; i < songs.size(); i++) { + if (id.equals(songs.get(i).getId())) { + positions.add(i); + } + } + return positions; + } + public Child getItem(int id) { return songs.get(id); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java index c770b607..f782c553 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java @@ -1,6 +1,8 @@ package com.cappielloantonio.tempo.ui.adapter; +import android.app.Activity; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -17,8 +19,6 @@ import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.databinding.ItemHorizontalTrackBinding; import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.interfaces.ClickCallback; -import com.cappielloantonio.tempo.interfaces.MediaSongIdCallback; -import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.DiscTitle; @@ -45,7 +45,10 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter songsFull; private List songs; private String currentFilter; - private ListenableFuture mediaBrowserListenableFuture; + + private String currentPlayingId; + private boolean isPlaying; + private List currentPlayingPositions = Collections.emptyList(); private final Filter filtering = new Filter() { @Override @@ -75,6 +78,12 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter) results.values; notifyDataSetChanged(); + + for (int pos : currentPlayingPositions) { + if (pos >= 0 && pos < songs.size()) { + notifyItemChanged(pos, "payload_playback"); + } + } } }; @@ -86,6 +95,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter payloads) { + if (!payloads.isEmpty() && payloads.contains("payload_playback")) { + bindPlaybackState(holder, songs.get(position)); + } else { + super.onBindViewHolder(holder, position, payloads); + } + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { Child song = songs.get(position); holder.item.searchResultSongTitleTextView.setText(song.getTitle()); @@ -171,20 +190,46 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter { + Activity a = (Activity) v.getContext(); + View root = a.findViewById(android.R.id.content); + View exoPlayPause = root.findViewById(R.id.exo_play_pause); + if (exoPlayPause != null) exoPlayPause.performClick(); + }); + bindPlaybackState(holder, song); + } + + private void bindPlaybackState(@NonNull ViewHolder holder, @NonNull Child song) { + boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId()) && isPlaying; + + if (isCurrent) { + holder.item.playPauseButton.setVisibility(View.VISIBLE); + holder.item.playPauseButton.setChecked(true); + if (!showCoverArt) { + holder.item.trackNumberTextView.setVisibility(View.GONE); + } else { + holder.item.coverArtOverlay.setVisibility(View.VISIBLE); + } + } else { + boolean sameIdPaused = currentPlayingId != null && currentPlayingId.equals(song.getId()) && !isPlaying; + if (sameIdPaused) { + holder.item.playPauseButton.setVisibility(View.VISIBLE); + holder.item.playPauseButton.setChecked(false); + if (!showCoverArt) { + holder.item.trackNumberTextView.setVisibility(View.GONE); } else { - holder.item.playPauseIcon.setVisibility(View.INVISIBLE); - if (showCoverArt) holder.item.coverArtOverlay.setVisibility(View.INVISIBLE); - if (!showCoverArt) holder.item.trackNumberTextView.setVisibility(View.VISIBLE); + holder.item.coverArtOverlay.setVisibility(View.VISIBLE); + } + } else { + holder.item.playPauseButton.setVisibility(View.GONE); + holder.item.playPauseButton.setChecked(false); + if (!showCoverArt) { + holder.item.trackNumberTextView.setVisibility(View.VISIBLE); + } else { + holder.item.coverArtOverlay.setVisibility(View.INVISIBLE); } } - }); + } } @Override @@ -195,7 +240,6 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter songs) { this.songsFull = songs != null ? songs : Collections.emptyList(); filtering.filter(currentFilter); - notifyDataSetChanged(); } @Override @@ -208,6 +252,46 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter oldPositions = currentPlayingPositions; + + this.currentPlayingId = mediaId; + this.isPlaying = playing; + + if (Objects.equals(oldId, mediaId) && oldPlaying == playing) { + List newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList(); + if (oldPositions.equals(newPositionsCheck)) { + return; + } + } + + currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList(); + + for (int pos : oldPositions) { + if (pos >= 0 && pos < songs.size()) { + notifyItemChanged(pos, "payload_playback"); + } + } + for (int pos : currentPlayingPositions) { + if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) { + notifyItemChanged(pos, "payload_playback"); + } + } + } + + private List findPositionsById(String id) { + if (id == null) return Collections.emptyList(); + List positions = new ArrayList<>(); + for (int i = 0; i < songs.size(); i++) { + if (id.equals(songs.get(i).getId())) { + positions.add(i); + } + } + return positions; + } + @Override public Filter getFilter() { return filtering; @@ -236,18 +320,21 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition()))); - bundle.putInt(Constants.ITEM_POSITION, MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition())); - + bundle.putParcelableArrayList( + Constants.TRACKS_OBJECT, + new ArrayList<>(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition())) + ); + bundle.putInt( + Constants.ITEM_POSITION, + MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition()) + ); click.onMediaClick(bundle); } private boolean onLongClick() { Bundle bundle = new Bundle(); bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition())); - click.onMediaLongClick(bundle); - return true; } } @@ -267,8 +354,4 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter mediaBrowserListenableFuture) { - this.mediaBrowserListenableFuture = mediaBrowserListenableFuture; - } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java index a7326b09..e2eb22e1 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java @@ -40,6 +40,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; @@ -52,6 +53,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { private FragmentAlbumPageBinding bind; private MainActivity activity; private AlbumPageViewModel albumPageViewModel; + private PlaybackViewModel playbackViewModel; private SongHorizontalAdapter songHorizontalAdapter; private ListenableFuture mediaBrowserListenableFuture; @@ -74,6 +76,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { bind = FragmentAlbumPageBinding.inflate(inflater, container, false); View view = bind.getRoot(); albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); init(); initAppBar(); @@ -91,12 +94,9 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { super.onStart(); initializeMediaBrowser(); - } - @Override - public void onResume() { - super.onResume(); - setMediaBrowserListenableFuture(); + MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override @@ -277,9 +277,12 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { songHorizontalAdapter = new SongHorizontalAdapter(this, false, false, album); bind.songRecyclerView.setAdapter(songHorizontalAdapter); - setMediaBrowserListenableFuture(); + reapplyPlayback(); - albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs)); + albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> { + songHorizontalAdapter.setItems(songs); + reapplyPlayback(); + }); } }); } @@ -295,7 +298,6 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { @Override public void onMediaClick(Bundle bundle) { MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); - songHorizontalAdapter.notifyDataSetChanged(); activity.setBottomSheetInPeek(true); } @@ -304,9 +306,26 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); } - private void setMediaBrowserListenableFuture() { + private void observePlayback() { + playbackViewModel.getCurrentMediaId().observe(getViewLifecycleOwner(), id -> { + if (songHorizontalAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { if (songHorizontalAdapter != null) { - songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + String id = playbackViewModel.getCurrentMediaId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); } } } \ No newline at end of file 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 65473ec4..ec9456cb 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 @@ -38,6 +38,7 @@ import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; @@ -49,6 +50,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { private FragmentArtistPageBinding bind; private MainActivity activity; private ArtistPageViewModel artistPageViewModel; + private PlaybackViewModel playbackViewModel; private SongHorizontalAdapter songHorizontalAdapter; private AlbumCatalogueAdapter albumCatalogueAdapter; @@ -63,6 +65,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { bind = FragmentArtistPageBinding.inflate(inflater, container, false); View view = bind.getRoot(); artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); init(); initAppBar(); @@ -80,12 +83,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { super.onStart(); initializeMediaBrowser(); - } - - @Override - public void onResume() { - super.onResume(); - setMediaBrowserListenableFuture(); + MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override @@ -180,7 +179,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null); bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter); - setMediaBrowserListenableFuture(); + reapplyPlayback(); artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> { if (songs == null) { if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE); @@ -190,6 +189,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { if (bind != null) bind.artistPageShuffleButton.setEnabled(!songs.isEmpty()); songHorizontalAdapter.setItems(songs); + reapplyPlayback(); } }); } @@ -253,7 +253,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { @Override public void onMediaClick(Bundle bundle) { MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); - songHorizontalAdapter.notifyDataSetChanged(); activity.setBottomSheetInPeek(true); } @@ -282,9 +281,26 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); } - private void setMediaBrowserListenableFuture() { + private void observePlayback() { + playbackViewModel.getCurrentMediaId().observe(getViewLifecycleOwner(), id -> { + if (songHorizontalAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { if (songHorizontalAdapter != null) { - songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + String id = playbackViewModel.getCurrentMediaId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); } } } \ No newline at end of file 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 c7ba117d..b779b0fd 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 @@ -60,6 +60,7 @@ import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.UIUtil; import com.cappielloantonio.tempo.viewmodel.HomeViewModel; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; import com.google.android.material.snackbar.Snackbar; import com.google.common.util.concurrent.ListenableFuture; @@ -74,6 +75,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { private FragmentHomeTabMusicBinding bind; private MainActivity activity; private HomeViewModel homeViewModel; + private PlaybackViewModel playbackViewModel; private DiscoverSongAdapter discoverSongAdapter; private SimilarTrackAdapter similarMusicAdapter; @@ -101,6 +103,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { bind = FragmentHomeTabMusicBinding.inflate(inflater, container, false); View view = bind.getRoot(); homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); init(); @@ -138,14 +141,16 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { super.onStart(); initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel); + observeStarredSongsPlayback(); + observeTopSongsPlayback(); } @Override public void onResume() { super.onResume(); refreshSharesView(); - setTopSongMediaBrowserListenableFuture(); - setStarredSongMediaBrowserListenableFuture(); } @Override @@ -479,7 +484,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { topSongAdapter = new SongHorizontalAdapter(this, true, false, null); bind.topSongsRecyclerView.setAdapter(topSongAdapter); - setTopSongMediaBrowserListenableFuture(); + reapplyTopSongsPlayback(); homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> { if (chronologies == null || chronologies.isEmpty()) { if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE); @@ -495,6 +500,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { .collect(Collectors.toList()); topSongAdapter.setItems(topSongs); + reapplyTopSongsPlayback(); } }); @@ -518,7 +524,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { starredSongAdapter = new SongHorizontalAdapter(this, true, false, null); bind.starredTracksRecyclerView.setAdapter(starredSongAdapter); - setStarredSongMediaBrowserListenableFuture(); + reapplyStarredSongsPlayback(); homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> { if (songs == null) { if (bind != null) bind.starredTracksSector.setVisibility(View.GONE); @@ -529,6 +535,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { bind.starredTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(songs.size(), 5), GridLayoutManager.HORIZONTAL, false)); starredSongAdapter.setItems(songs); + reapplyStarredSongsPlayback(); } }); @@ -1050,15 +1057,49 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { Navigation.findNavController(requireView()).navigate(R.id.shareBottomSheetDialog, bundle); } - private void setTopSongMediaBrowserListenableFuture() { - if (topSongAdapter != null) { - topSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + private void observeStarredSongsPlayback() { + playbackViewModel.getCurrentMediaId().observe(getViewLifecycleOwner(), id -> { + if (starredSongAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + starredSongAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (starredSongAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + starredSongAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void observeTopSongsPlayback() { + playbackViewModel.getCurrentMediaId().observe(getViewLifecycleOwner(), id -> { + if (topSongAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + topSongAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (topSongAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + topSongAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyStarredSongsPlayback() { + if (starredSongAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + starredSongAdapter.setPlaybackState(id, playing != null && playing); } } - private void setStarredSongMediaBrowserListenableFuture() { - if (starredSongAdapter != null) { - starredSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + private void reapplyTopSongsPlayback() { + if (topSongAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + topSongAdapter.setPlaybackState(id, playing != null && playing); } } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java index 69e0d4fc..09f91734 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java @@ -23,6 +23,7 @@ import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter; import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -38,6 +39,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { private InnerFragmentPlayerQueueBinding bind; private PlayerBottomSheetViewModel playerBottomSheetViewModel; + private PlaybackViewModel playbackViewModel; private ListenableFuture mediaBrowserListenableFuture; private PlayerSongQueueAdapter playerSongQueueAdapter; @@ -48,6 +50,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { View view = bind.getRoot(); playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); initQueueRecyclerView(); @@ -59,13 +62,15 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { super.onStart(); initializeBrowser(); bindMediaController(); + + MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override public void onResume() { super.onResume(); setMediaBrowserListenableFuture(); - updateNowPlayingItem(); } @Override @@ -110,10 +115,12 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { playerSongQueueAdapter = new PlayerSongQueueAdapter(this); bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter); - setMediaBrowserListenableFuture(); + reapplyPlayback(); + playerBottomSheetViewModel.getQueueSong().observe(getViewLifecycleOwner(), queue -> { if (queue != null) { playerSongQueueAdapter.setItems(queue.stream().map(item -> (Child) item).collect(Collectors.toList())); + reapplyPlayback(); } }); @@ -209,13 +216,31 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { }); } - private void updateNowPlayingItem() { - playerSongQueueAdapter.notifyDataSetChanged(); - } - @Override public void onMediaClick(Bundle bundle) { MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); - updateNowPlayingItem(); + } + + private void observePlayback() { + playbackViewModel.getCurrentMediaId().observe(getViewLifecycleOwner(), id -> { + if (playerSongQueueAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + playerSongQueueAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (playerSongQueueAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + playerSongQueueAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (playerSongQueueAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + playerSongQueueAdapter.setPlaybackState(id, playing != null && playing); + } } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java index 9da26370..b3eb97ee 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java @@ -37,6 +37,7 @@ import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel; import com.google.common.util.concurrent.ListenableFuture; @@ -49,6 +50,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { private FragmentPlaylistPageBinding bind; private MainActivity activity; private PlaylistPageViewModel playlistPageViewModel; + private PlaybackViewModel playbackViewModel; private SongHorizontalAdapter songHorizontalAdapter; @@ -94,6 +96,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { bind = FragmentPlaylistPageBinding.inflate(inflater, container, false); View view = bind.getRoot(); playlistPageViewModel = new ViewModelProvider(requireActivity()).get(PlaylistPageViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); init(); initAppBar(); @@ -109,12 +112,9 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { super.onStart(); initializeMediaBrowser(); - } - @Override - public void onResume() { - super.onResume(); - setMediaBrowserListenableFuture(); + MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override @@ -254,9 +254,12 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null); bind.songRecyclerView.setAdapter(songHorizontalAdapter); - setMediaBrowserListenableFuture(); + reapplyPlayback(); - playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs)); + playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> { + songHorizontalAdapter.setItems(songs); + reapplyPlayback(); + }); } private void initializeMediaBrowser() { @@ -270,7 +273,6 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { @Override public void onMediaClick(Bundle bundle) { MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); - songHorizontalAdapter.notifyDataSetChanged(); activity.setBottomSheetInPeek(true); } @@ -279,9 +281,26 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); } - private void setMediaBrowserListenableFuture() { + private void observePlayback() { + playbackViewModel.getCurrentMediaId().observe(getViewLifecycleOwner(), id -> { + if (songHorizontalAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { if (songHorizontalAdapter != null) { - songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + String id = playbackViewModel.getCurrentMediaId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); } } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java index a67c6c02..25268f0d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java @@ -34,6 +34,7 @@ import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter; import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; import com.cappielloantonio.tempo.viewmodel.SearchViewModel; import com.google.common.util.concurrent.ListenableFuture; @@ -46,6 +47,7 @@ public class SearchFragment extends Fragment implements ClickCallback { private FragmentSearchBinding bind; private MainActivity activity; private SearchViewModel searchViewModel; + private PlaybackViewModel playbackViewModel; private ArtistAdapter artistAdapter; private AlbumAdapter albumAdapter; @@ -61,6 +63,7 @@ public class SearchFragment extends Fragment implements ClickCallback { bind = FragmentSearchBinding.inflate(inflater, container, false); View view = bind.getRoot(); searchViewModel = new ViewModelProvider(requireActivity()).get(SearchViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); initSearchResultView(); initSearchView(); @@ -73,12 +76,14 @@ public class SearchFragment extends Fragment implements ClickCallback { public void onStart() { super.onStart(); initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override public void onResume() { super.onResume(); - setMediaBrowserListenableFuture(); } @Override @@ -119,7 +124,8 @@ public class SearchFragment extends Fragment implements ClickCallback { bind.searchResultTracksRecyclerView.setHasFixedSize(true); songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null); - setMediaBrowserListenableFuture(); + reapplyPlayback(); + bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter); } @@ -296,9 +302,26 @@ public class SearchFragment extends Fragment implements ClickCallback { Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); } - private void setMediaBrowserListenableFuture() { + private void observePlayback() { + playbackViewModel.getCurrentMediaId().observe(getViewLifecycleOwner(), id -> { + if (songHorizontalAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { if (songHorizontalAdapter != null) { - songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + String id = playbackViewModel.getCurrentMediaId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); } } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java index 3c75f1da..35a09d4c 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java @@ -36,6 +36,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel; import com.google.common.util.concurrent.ListenableFuture; @@ -49,6 +50,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback { private FragmentSongListPageBinding bind; private MainActivity activity; private SongListPageViewModel songListPageViewModel; + private PlaybackViewModel playbackViewModel; private SongHorizontalAdapter songHorizontalAdapter; @@ -69,6 +71,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback { bind = FragmentSongListPageBinding.inflate(inflater, container, false); View view = bind.getRoot(); songListPageViewModel = new ViewModelProvider(requireActivity()).get(SongListPageViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); init(); initAppBar(); @@ -82,12 +85,9 @@ public class SongListPageFragment extends Fragment implements ClickCallback { public void onStart() { super.onStart(); initializeMediaBrowser(); - } - @Override - public void onResume() { - super.onResume(); - setMediaBrowserListenableFuture(); + MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override @@ -197,10 +197,11 @@ public class SongListPageFragment extends Fragment implements ClickCallback { songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null); bind.songListRecyclerView.setAdapter(songHorizontalAdapter); - setMediaBrowserListenableFuture(); + reapplyPlayback(); songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> { isLoading = false; songHorizontalAdapter.setItems(songs); + reapplyPlayback(); setSongListPageSubtitle(songs); }); @@ -325,7 +326,6 @@ public class SongListPageFragment extends Fragment implements ClickCallback { public void onMediaClick(Bundle bundle) { hideKeyboard(requireView()); MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); - songHorizontalAdapter.notifyDataSetChanged(); activity.setBottomSheetInPeek(true); } @@ -334,9 +334,26 @@ public class SongListPageFragment extends Fragment implements ClickCallback { Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); } - private void setMediaBrowserListenableFuture() { + private void observePlayback() { + playbackViewModel.getCurrentMediaId().observe(getViewLifecycleOwner(), id -> { + if (songHorizontalAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentMediaId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { if (songHorizontalAdapter != null) { - songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + String id = playbackViewModel.getCurrentMediaId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); } } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaybackViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaybackViewModel.java new file mode 100644 index 00000000..a6733e72 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaybackViewModel.java @@ -0,0 +1,38 @@ +package com.cappielloantonio.tempo.viewmodel; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import java.util.Objects; + +public class PlaybackViewModel extends ViewModel { + + private final MutableLiveData currentMediaId = new MutableLiveData<>(null); + private final MutableLiveData isPlaying = new MutableLiveData<>(false); + + // (Optional) expose position or other info + // private final MutableLiveData positionMs = new MutableLiveData<>(0L); + + public LiveData getCurrentMediaId() { + return currentMediaId; + } + + public LiveData getIsPlaying() { + return isPlaying; + } + + public void update(String mediaId, boolean playing) { + if (!Objects.equals(currentMediaId.getValue(), mediaId)) { + currentMediaId.postValue(mediaId); + } + if (!Objects.equals(isPlaying.getValue(), playing)) { + isPlaying.postValue(playing); + } + } + + public void clear() { + currentMediaId.postValue(null); + isPlaying.postValue(false); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/item_horizontal_track.xml b/app/src/main/res/layout/item_horizontal_track.xml index 69420c51..57711a1c 100644 --- a/app/src/main/res/layout/item_horizontal_track.xml +++ b/app/src/main/res/layout/item_horizontal_track.xml @@ -66,16 +66,21 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" /> - + app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" /> - + app:layout_constraintTop_toTopOf="parent" />