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 ea1dcaba..b8a6008f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -1,11 +1,16 @@ package com.cappielloantonio.tempo.service; import android.content.ComponentName; +import android.util.Log; +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; @@ -21,14 +26,79 @@ 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); + + public static void registerPlaybackObserver( + 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.e(TAG, "Failed to get MediaBrowser instance", t); + } + }, MoreExecutors.directExecutor()); + } + + 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) { 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 5747eab1..9a35fd92 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 @@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter; 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; @@ -23,17 +24,24 @@ 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(); @@ -104,6 +112,50 @@ 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() { @@ -132,6 +184,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 a1630bca..9f606e9a 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; @@ -10,6 +12,7 @@ import android.widget.Filterable; import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; import androidx.recyclerview.widget.RecyclerView; import com.cappielloantonio.tempo.R; @@ -23,6 +26,7 @@ import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.Preferences; +import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; import java.util.Collections; @@ -42,6 +46,10 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter songs; private String currentFilter; + private String currentPlayingId; + private boolean isPlaying; + private List currentPlayingPositions = Collections.emptyList(); + private final Filter filtering = new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { @@ -70,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"); + } + } } }; @@ -81,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()); @@ -165,6 +189,47 @@ 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.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 @@ -188,6 +253,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; 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 03e71e10..50a09fe7 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,6 +94,9 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { super.onStart(); initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override @@ -271,8 +277,12 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { songHorizontalAdapter = new SongHorizontalAdapter(this, false, false, album); bind.songRecyclerView.setAdapter(songHorizontalAdapter); + reapplyPlayback(); - albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs)); + albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> { + songHorizontalAdapter.setItems(songs); + reapplyPlayback(); + }); } }); } @@ -295,4 +305,27 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { public void onMediaLongClick(Bundle bundle) { Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().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.getCurrentSongId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().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 9dccc83e..1d1cb7d1 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 @@ -29,19 +29,16 @@ import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.ui.activity.MainActivity; -import com.cappielloantonio.tempo.ui.adapter.AlbumArtistPageOrSimilarAdapter; import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter; import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter; -import com.cappielloantonio.tempo.ui.adapter.ArtistSimilarAdapter; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; 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; -import java.util.Collections; import java.util.List; @UnstableApi @@ -49,6 +46,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 +61,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,6 +79,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { super.onStart(); initializeMediaBrowser(); + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override @@ -174,6 +175,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null); bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter); + reapplyPlayback(); artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> { if (songs == null) { if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE); @@ -183,6 +185,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { if (bind != null) bind.artistPageShuffleButton.setEnabled(!songs.isEmpty()); songHorizontalAdapter.setItems(songs); + reapplyPlayback(); } }); } @@ -273,4 +276,27 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { public void onArtistLongClick(Bundle bundle) { Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().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.getCurrentSongId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().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 4d30ce30..ae95f35f 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,6 +141,10 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { super.onStart(); initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observeStarredSongsPlayback(); + observeTopSongsPlayback(); } @Override @@ -477,6 +484,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { topSongAdapter = new SongHorizontalAdapter(this, true, false, null); bind.topSongsRecyclerView.setAdapter(topSongAdapter); + reapplyTopSongsPlayback(); homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> { if (chronologies == null || chronologies.isEmpty()) { if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE); @@ -492,6 +500,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { .collect(Collectors.toList()); topSongAdapter.setItems(topSongs); + reapplyTopSongsPlayback(); } }); @@ -515,6 +524,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { starredSongAdapter = new SongHorizontalAdapter(this, true, false, null); bind.starredTracksRecyclerView.setAdapter(starredSongAdapter); + reapplyStarredSongsPlayback(); homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> { if (songs == null) { if (bind != null) bind.starredTracksSector.setVisibility(View.GONE); @@ -525,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(); } }); @@ -954,6 +965,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); activity.setBottomSheetInPeek(true); } + topSongAdapter.notifyDataSetChanged(); + starredSongAdapter.notifyDataSetChanged(); } @Override @@ -1043,4 +1056,50 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { public void onShareLongClick(Bundle bundle) { Navigation.findNavController(requireView()).navigate(R.id.shareBottomSheetDialog, bundle); } + + private void observeStarredSongsPlayback() { + playbackViewModel.getCurrentSongId().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.getCurrentSongId().getValue(); + starredSongAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void observeTopSongsPlayback() { + playbackViewModel.getCurrentSongId().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.getCurrentSongId().getValue(); + topSongAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyStarredSongsPlayback() { + if (starredSongAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + starredSongAdapter.setPlaybackState(id, playing != null && playing); + } + } + + private void reapplyTopSongsPlayback() { + if (topSongAdapter != null) { + String id = playbackViewModel.getCurrentSongId().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 f53a2247..06536cd6 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,6 +62,9 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { super.onStart(); initializeBrowser(); bindMediaController(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override @@ -110,9 +116,12 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { playerSongQueueAdapter = new PlayerSongQueueAdapter(this); bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter); + reapplyPlayback(); + playerBottomSheetViewModel.getQueueSong().observe(getViewLifecycleOwner(), queue -> { if (queue != null) { playerSongQueueAdapter.setItems(queue.stream().map(item -> (Child) item).collect(Collectors.toList())); + reapplyPlayback(); } }); @@ -216,4 +225,27 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { public void onMediaClick(Bundle bundle) { MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().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.getCurrentSongId().getValue(); + playerSongQueueAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (playerSongQueueAdapter != null) { + String id = playbackViewModel.getCurrentSongId().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 55b46ff2..ef58e96c 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,6 +112,9 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { super.onStart(); initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override @@ -248,8 +254,12 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null); bind.songRecyclerView.setAdapter(songHorizontalAdapter); + reapplyPlayback(); - playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs)); + playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> { + songHorizontalAdapter.setItems(songs); + reapplyPlayback(); + }); } private void initializeMediaBrowser() { @@ -270,4 +280,27 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { public void onMediaLongClick(Bundle bundle) { Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().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.getCurrentSongId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().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 8792a98a..b6c3b8d3 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 @@ -4,14 +4,11 @@ import android.content.ComponentName; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -34,6 +31,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 +44,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 +60,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,6 +73,14 @@ public class SearchFragment extends Fragment implements ClickCallback { public void onStart() { super.onStart(); initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); + } + + @Override + public void onResume() { + super.onResume(); } @Override @@ -113,6 +121,8 @@ public class SearchFragment extends Fragment implements ClickCallback { bind.searchResultTracksRecyclerView.setHasFixedSize(true); songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null); + reapplyPlayback(); + bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter); } @@ -260,6 +270,7 @@ public class SearchFragment 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); } @@ -287,4 +298,27 @@ public class SearchFragment extends Fragment implements ClickCallback { public void onArtistLongClick(Bundle bundle) { Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().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.getCurrentSongId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().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 fcaeec84..4f8bad46 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,6 +85,9 @@ public class SongListPageFragment extends Fragment implements ClickCallback { public void onStart() { super.onStart(); initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); } @Override @@ -191,9 +197,11 @@ public class SongListPageFragment extends Fragment implements ClickCallback { songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null); bind.songListRecyclerView.setAdapter(songHorizontalAdapter); + reapplyPlayback(); songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> { isLoading = false; songHorizontalAdapter.setItems(songs); + reapplyPlayback(); setSongListPageSubtitle(songs); }); @@ -325,4 +333,27 @@ public class SongListPageFragment extends Fragment implements ClickCallback { public void onMediaLongClick(Bundle bundle) { Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().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.getCurrentSongId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().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..b1808d9f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaybackViewModel.java @@ -0,0 +1,35 @@ +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 currentSongId = new MutableLiveData<>(null); + private final MutableLiveData isPlaying = new MutableLiveData<>(false); + + public LiveData getCurrentSongId() { + return currentSongId; + } + + public LiveData getIsPlaying() { + return isPlaying; + } + + public void update(String songId, boolean playing) { + if (!Objects.equals(currentSongId.getValue(), songId)) { + currentSongId.postValue(songId); + } + if (!Objects.equals(isPlaying.getValue(), playing)) { + isPlaying.postValue(playing); + } + } + + public void clear() { + currentSongId.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 dfea07a9..57711a1c 100644 --- a/app/src/main/res/layout/item_horizontal_track.xml +++ b/app/src/main/res/layout/item_horizontal_track.xml @@ -55,6 +55,33 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" /> + + + + + + + +