feat: Add play/pause button in song lists

This commit is contained in:
Jaime García 2025-09-22 19:28:01 +02:00
parent 905bb3e3c5
commit 5ab68e4a98
No known key found for this signature in database
GPG key ID: BC4E5F71A71BDA5B
14 changed files with 555 additions and 135 deletions

View file

@ -1,8 +0,0 @@
package com.cappielloantonio.tempo.interfaces;
import androidx.annotation.Keep;
@Keep
public interface MediaSongIdCallback {
default void onRecovery(String id) {}
}

View file

@ -2,17 +2,20 @@ package com.cappielloantonio.tempo.service;
import android.content.ComponentName; import android.content.ComponentName;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn; import androidx.annotation.OptIn;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser; import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken; import androidx.media3.session.SessionToken;
import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback; import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
import com.cappielloantonio.tempo.interfaces.MediaSongIdCallback;
import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.repository.ChronologyRepository; import com.cappielloantonio.tempo.repository.ChronologyRepository;
import com.cappielloantonio.tempo.repository.QueueRepository; 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.subsonic.models.PodcastEpisode;
import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.Preferences; 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.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import java.lang.ref.WeakReference;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
public class MediaManager { public class MediaManager {
private static final String TAG = "MediaManager"; private static final String TAG = "MediaManager";
private static WeakReference<MediaBrowser> 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<MediaBrowser> browserFuture,
PlaybackViewModel playbackViewModel
) {
if (browserFuture == null) return;
Futures.addCallback(browserFuture, new FutureCallback<MediaBrowser>() {
@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<MediaBrowser> mediaBrowserListenableFuture) { public static void reset(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture) {
if (mediaBrowserListenableFuture != null) { if (mediaBrowserListenableFuture != null) {
@ -293,25 +370,6 @@ public class MediaManager {
} }
} }
public static void getCurrentSongId(ListenableFuture<MediaBrowser> 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) { public static void setLastPlayedTimestamp(MediaItem mediaItem) {
if (mediaItem != null) getQueueRepository().setLastPlayedTimestamp(mediaItem.mediaId); if (mediaItem != null) getQueueRepository().setLastPlayedTimestamp(mediaItem.mediaId);
} }

View file

@ -1,7 +1,9 @@
package com.cappielloantonio.tempo.ui.adapter; package com.cappielloantonio.tempo.ui.adapter;
import android.app.Activity;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -17,24 +19,30 @@ import com.cappielloantonio.tempo.databinding.ItemPlayerQueueSongBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback; import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
import com.cappielloantonio.tempo.interfaces.MediaSongIdCallback;
import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueueAdapter.ViewHolder> { public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueueAdapter.ViewHolder> {
private static final String TAG = "PlayerSongQueueAdapter";
private final ClickCallback click; private final ClickCallback click;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture; private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private List<Child> songs; private List<Child> songs;
private String currentPlayingId;
private boolean isPlaying;
private List<Integer> currentPlayingPositions = Collections.emptyList();
public PlayerSongQueueAdapter(ClickCallback click) { public PlayerSongQueueAdapter(ClickCallback click) {
this.click = click; this.click = click;
this.songs = Collections.emptyList(); this.songs = Collections.emptyList();
@ -87,19 +95,6 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
} }
}); });
MediaManager.getCurrentSongId(mediaBrowserListenableFuture, new MediaSongIdCallback() {
@Override
public void onRecovery(String id) {
if (song.getId().equals(id)) {
holder.item.playPauseIcon.setVisibility(View.VISIBLE);
holder.item.coverArtOverlay.setVisibility(View.VISIBLE);
} else {
holder.item.playPauseIcon.setVisibility(View.INVISIBLE);
holder.item.coverArtOverlay.setVisibility(View.INVISIBLE);
}
}
});
if (Preferences.showItemRating()) { if (Preferences.showItemRating()) {
if (song.getStarred() == null && song.getUserRating() == null) { if (song.getStarred() == null && song.getUserRating() == null) {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE); holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
@ -118,6 +113,50 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
} else { } else {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE); holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
} }
holder.item.playPauseButton.setOnClickListener(v -> {
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<Child> getItems() { public List<Child> getItems() {
@ -146,6 +185,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture; this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
} }
public void setPlaybackState(String mediaId, boolean playing) {
String oldId = this.currentPlayingId;
boolean oldPlaying = this.isPlaying;
List<Integer> oldPositions = currentPlayingPositions;
this.currentPlayingId = mediaId;
this.isPlaying = playing;
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
List<Integer> 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<Integer> findPositionsById(String id) {
if (id == null) return Collections.emptyList();
List<Integer> 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) { public Child getItem(int id) {
return songs.get(id); return songs.get(id);
} }

View file

@ -1,6 +1,8 @@
package com.cappielloantonio.tempo.ui.adapter; package com.cappielloantonio.tempo.ui.adapter;
import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -17,8 +19,6 @@ import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.ItemHorizontalTrackBinding; import com.cappielloantonio.tempo.databinding.ItemHorizontalTrackBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback; 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.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.DiscTitle; import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
@ -45,7 +45,10 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
private List<Child> songsFull; private List<Child> songsFull;
private List<Child> songs; private List<Child> songs;
private String currentFilter; private String currentFilter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private String currentPlayingId;
private boolean isPlaying;
private List<Integer> currentPlayingPositions = Collections.emptyList();
private final Filter filtering = new Filter() { private final Filter filtering = new Filter() {
@Override @Override
@ -75,6 +78,12 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
protected void publishResults(CharSequence constraint, FilterResults results) { protected void publishResults(CharSequence constraint, FilterResults results) {
songs = (List<Child>) results.values; songs = (List<Child>) results.values;
notifyDataSetChanged(); 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<SongHorizontalAd
this.songsFull = Collections.emptyList(); this.songsFull = Collections.emptyList();
this.currentFilter = ""; this.currentFilter = "";
this.album = album; this.album = album;
setHasStableIds(false);
} }
@NonNull @NonNull
@ -96,7 +106,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
} }
@Override @Override
public void onBindViewHolder(ViewHolder holder, int position) { public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> 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); Child song = songs.get(position);
holder.item.searchResultSongTitleTextView.setText(song.getTitle()); holder.item.searchResultSongTitleTextView.setText(song.getTitle());
@ -171,20 +190,46 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
holder.item.ratingIndicatorImageView.setVisibility(View.GONE); holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
} }
MediaManager.getCurrentSongId(mediaBrowserListenableFuture, new MediaSongIdCallback() { holder.item.playPauseButton.setOnClickListener(v -> {
@Override Activity a = (Activity) v.getContext();
public void onRecovery(String id) { View root = a.findViewById(android.R.id.content);
if (song.getId().equals(id)) { View exoPlayPause = root.findViewById(R.id.exo_play_pause);
holder.item.playPauseIcon.setVisibility(View.VISIBLE); if (exoPlayPause != null) exoPlayPause.performClick();
if (!showCoverArt) holder.item.trackNumberTextView.setVisibility(View.INVISIBLE); });
if (showCoverArt) holder.item.coverArtOverlay.setVisibility(View.VISIBLE); 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 { } else {
holder.item.playPauseIcon.setVisibility(View.INVISIBLE); holder.item.coverArtOverlay.setVisibility(View.VISIBLE);
if (showCoverArt) holder.item.coverArtOverlay.setVisibility(View.INVISIBLE); }
if (!showCoverArt) holder.item.trackNumberTextView.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 @Override
@ -195,7 +240,6 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
public void setItems(List<Child> songs) { public void setItems(List<Child> songs) {
this.songsFull = songs != null ? songs : Collections.emptyList(); this.songsFull = songs != null ? songs : Collections.emptyList();
filtering.filter(currentFilter); filtering.filter(currentFilter);
notifyDataSetChanged();
} }
@Override @Override
@ -208,6 +252,46 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
return position; return position;
} }
public void setPlaybackState(String mediaId, boolean playing) {
String oldId = this.currentPlayingId;
boolean oldPlaying = this.isPlaying;
List<Integer> oldPositions = currentPlayingPositions;
this.currentPlayingId = mediaId;
this.isPlaying = playing;
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
List<Integer> 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<Integer> findPositionsById(String id) {
if (id == null) return Collections.emptyList();
List<Integer> positions = new ArrayList<>();
for (int i = 0; i < songs.size(); i++) {
if (id.equals(songs.get(i).getId())) {
positions.add(i);
}
}
return positions;
}
@Override @Override
public Filter getFilter() { public Filter getFilter() {
return filtering; return filtering;
@ -236,18 +320,21 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
public void onClick() { public void onClick() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition()))); bundle.putParcelableArrayList(
bundle.putInt(Constants.ITEM_POSITION, MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition())); Constants.TRACKS_OBJECT,
new ArrayList<>(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition()))
);
bundle.putInt(
Constants.ITEM_POSITION,
MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition())
);
click.onMediaClick(bundle); click.onMediaClick(bundle);
} }
private boolean onLongClick() { private boolean onLongClick() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition())); bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
click.onMediaLongClick(bundle); click.onMediaLongClick(bundle);
return true; return true;
} }
} }
@ -267,8 +354,4 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
notifyDataSetChanged(); notifyDataSetChanged();
} }
public void setMediaBrowserListenableFuture(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture) {
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
}
} }

View file

@ -40,6 +40,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel; import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList; import java.util.ArrayList;
@ -52,6 +53,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
private FragmentAlbumPageBinding bind; private FragmentAlbumPageBinding bind;
private MainActivity activity; private MainActivity activity;
private AlbumPageViewModel albumPageViewModel; private AlbumPageViewModel albumPageViewModel;
private PlaybackViewModel playbackViewModel;
private SongHorizontalAdapter songHorizontalAdapter; private SongHorizontalAdapter songHorizontalAdapter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture; private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@ -74,6 +76,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
bind = FragmentAlbumPageBinding.inflate(inflater, container, false); bind = FragmentAlbumPageBinding.inflate(inflater, container, false);
View view = bind.getRoot(); View view = bind.getRoot();
albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class); albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
init(); init();
initAppBar(); initAppBar();
@ -91,12 +94,9 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
super.onStart(); super.onStart();
initializeMediaBrowser(); initializeMediaBrowser();
}
@Override MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel);
public void onResume() { observePlayback();
super.onResume();
setMediaBrowserListenableFuture();
} }
@Override @Override
@ -277,9 +277,12 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
songHorizontalAdapter = new SongHorizontalAdapter(this, false, false, album); songHorizontalAdapter = new SongHorizontalAdapter(this, false, false, album);
bind.songRecyclerView.setAdapter(songHorizontalAdapter); 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 @Override
public void onMediaClick(Bundle bundle) { public void onMediaClick(Bundle bundle) {
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
songHorizontalAdapter.notifyDataSetChanged();
activity.setBottomSheetInPeek(true); activity.setBottomSheetInPeek(true);
} }
@ -304,9 +306,26 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); 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) { if (songHorizontalAdapter != null) {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); String id = playbackViewModel.getCurrentMediaId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
} }
} }
} }

View file

@ -38,6 +38,7 @@ import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel; import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList; import java.util.ArrayList;
@ -49,6 +50,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private FragmentArtistPageBinding bind; private FragmentArtistPageBinding bind;
private MainActivity activity; private MainActivity activity;
private ArtistPageViewModel artistPageViewModel; private ArtistPageViewModel artistPageViewModel;
private PlaybackViewModel playbackViewModel;
private SongHorizontalAdapter songHorizontalAdapter; private SongHorizontalAdapter songHorizontalAdapter;
private AlbumCatalogueAdapter albumCatalogueAdapter; private AlbumCatalogueAdapter albumCatalogueAdapter;
@ -63,6 +65,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
bind = FragmentArtistPageBinding.inflate(inflater, container, false); bind = FragmentArtistPageBinding.inflate(inflater, container, false);
View view = bind.getRoot(); View view = bind.getRoot();
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class); artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
init(); init();
initAppBar(); initAppBar();
@ -80,12 +83,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
super.onStart(); super.onStart();
initializeMediaBrowser(); initializeMediaBrowser();
} MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel);
observePlayback();
@Override
public void onResume() {
super.onResume();
setMediaBrowserListenableFuture();
} }
@Override @Override
@ -180,7 +179,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null); songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null);
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter); bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
setMediaBrowserListenableFuture(); reapplyPlayback();
artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> { artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> {
if (songs == null) { if (songs == null) {
if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE); if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE);
@ -190,6 +189,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
if (bind != null) if (bind != null)
bind.artistPageShuffleButton.setEnabled(!songs.isEmpty()); bind.artistPageShuffleButton.setEnabled(!songs.isEmpty());
songHorizontalAdapter.setItems(songs); songHorizontalAdapter.setItems(songs);
reapplyPlayback();
} }
}); });
} }
@ -253,7 +253,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
@Override @Override
public void onMediaClick(Bundle bundle) { public void onMediaClick(Bundle bundle) {
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
songHorizontalAdapter.notifyDataSetChanged();
activity.setBottomSheetInPeek(true); activity.setBottomSheetInPeek(true);
} }
@ -282,9 +281,26 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); 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) { if (songHorizontalAdapter != null) {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); String id = playbackViewModel.getCurrentMediaId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
} }
} }
} }

View file

@ -60,6 +60,7 @@ import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.UIUtil; import com.cappielloantonio.tempo.util.UIUtil;
import com.cappielloantonio.tempo.viewmodel.HomeViewModel; import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -74,6 +75,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
private FragmentHomeTabMusicBinding bind; private FragmentHomeTabMusicBinding bind;
private MainActivity activity; private MainActivity activity;
private HomeViewModel homeViewModel; private HomeViewModel homeViewModel;
private PlaybackViewModel playbackViewModel;
private DiscoverSongAdapter discoverSongAdapter; private DiscoverSongAdapter discoverSongAdapter;
private SimilarTrackAdapter similarMusicAdapter; private SimilarTrackAdapter similarMusicAdapter;
@ -101,6 +103,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
bind = FragmentHomeTabMusicBinding.inflate(inflater, container, false); bind = FragmentHomeTabMusicBinding.inflate(inflater, container, false);
View view = bind.getRoot(); View view = bind.getRoot();
homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class); homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
init(); init();
@ -138,14 +141,16 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
super.onStart(); super.onStart();
initializeMediaBrowser(); initializeMediaBrowser();
MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel);
observeStarredSongsPlayback();
observeTopSongsPlayback();
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
refreshSharesView(); refreshSharesView();
setTopSongMediaBrowserListenableFuture();
setStarredSongMediaBrowserListenableFuture();
} }
@Override @Override
@ -479,7 +484,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
topSongAdapter = new SongHorizontalAdapter(this, true, false, null); topSongAdapter = new SongHorizontalAdapter(this, true, false, null);
bind.topSongsRecyclerView.setAdapter(topSongAdapter); bind.topSongsRecyclerView.setAdapter(topSongAdapter);
setTopSongMediaBrowserListenableFuture(); reapplyTopSongsPlayback();
homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> { homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> {
if (chronologies == null || chronologies.isEmpty()) { if (chronologies == null || chronologies.isEmpty()) {
if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE); if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE);
@ -495,6 +500,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
.collect(Collectors.toList()); .collect(Collectors.toList());
topSongAdapter.setItems(topSongs); topSongAdapter.setItems(topSongs);
reapplyTopSongsPlayback();
} }
}); });
@ -518,7 +524,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
starredSongAdapter = new SongHorizontalAdapter(this, true, false, null); starredSongAdapter = new SongHorizontalAdapter(this, true, false, null);
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter); bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
setStarredSongMediaBrowserListenableFuture(); reapplyStarredSongsPlayback();
homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> { homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
if (songs == null) { if (songs == null) {
if (bind != null) bind.starredTracksSector.setVisibility(View.GONE); 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)); bind.starredTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(songs.size(), 5), GridLayoutManager.HORIZONTAL, false));
starredSongAdapter.setItems(songs); starredSongAdapter.setItems(songs);
reapplyStarredSongsPlayback();
} }
}); });
@ -1050,15 +1057,49 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
Navigation.findNavController(requireView()).navigate(R.id.shareBottomSheetDialog, bundle); Navigation.findNavController(requireView()).navigate(R.id.shareBottomSheetDialog, bundle);
} }
private void setTopSongMediaBrowserListenableFuture() { private void observeStarredSongsPlayback() {
if (topSongAdapter != null) { playbackViewModel.getCurrentMediaId().observe(getViewLifecycleOwner(), id -> {
topSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); 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() { private void reapplyTopSongsPlayback() {
if (starredSongAdapter != null) { if (topSongAdapter != null) {
starredSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); String id = playbackViewModel.getCurrentMediaId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
topSongAdapter.setPlaybackState(id, playing != null && playing);
} }
} }
} }

View file

@ -23,6 +23,7 @@ import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter; import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -38,6 +39,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
private InnerFragmentPlayerQueueBinding bind; private InnerFragmentPlayerQueueBinding bind;
private PlayerBottomSheetViewModel playerBottomSheetViewModel; private PlayerBottomSheetViewModel playerBottomSheetViewModel;
private PlaybackViewModel playbackViewModel;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture; private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private PlayerSongQueueAdapter playerSongQueueAdapter; private PlayerSongQueueAdapter playerSongQueueAdapter;
@ -48,6 +50,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
View view = bind.getRoot(); View view = bind.getRoot();
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
initQueueRecyclerView(); initQueueRecyclerView();
@ -59,13 +62,15 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
super.onStart(); super.onStart();
initializeBrowser(); initializeBrowser();
bindMediaController(); bindMediaController();
MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel);
observePlayback();
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
setMediaBrowserListenableFuture(); setMediaBrowserListenableFuture();
updateNowPlayingItem();
} }
@Override @Override
@ -110,10 +115,12 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
playerSongQueueAdapter = new PlayerSongQueueAdapter(this); playerSongQueueAdapter = new PlayerSongQueueAdapter(this);
bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter); bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter);
setMediaBrowserListenableFuture(); reapplyPlayback();
playerBottomSheetViewModel.getQueueSong().observe(getViewLifecycleOwner(), queue -> { playerBottomSheetViewModel.getQueueSong().observe(getViewLifecycleOwner(), queue -> {
if (queue != null) { if (queue != null) {
playerSongQueueAdapter.setItems(queue.stream().map(item -> (Child) item).collect(Collectors.toList())); 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 @Override
public void onMediaClick(Bundle bundle) { public void onMediaClick(Bundle bundle) {
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); 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);
}
} }
} }

View file

@ -37,6 +37,7 @@ import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel; import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -49,6 +50,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
private FragmentPlaylistPageBinding bind; private FragmentPlaylistPageBinding bind;
private MainActivity activity; private MainActivity activity;
private PlaylistPageViewModel playlistPageViewModel; private PlaylistPageViewModel playlistPageViewModel;
private PlaybackViewModel playbackViewModel;
private SongHorizontalAdapter songHorizontalAdapter; private SongHorizontalAdapter songHorizontalAdapter;
@ -94,6 +96,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
bind = FragmentPlaylistPageBinding.inflate(inflater, container, false); bind = FragmentPlaylistPageBinding.inflate(inflater, container, false);
View view = bind.getRoot(); View view = bind.getRoot();
playlistPageViewModel = new ViewModelProvider(requireActivity()).get(PlaylistPageViewModel.class); playlistPageViewModel = new ViewModelProvider(requireActivity()).get(PlaylistPageViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
init(); init();
initAppBar(); initAppBar();
@ -109,12 +112,9 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
super.onStart(); super.onStart();
initializeMediaBrowser(); initializeMediaBrowser();
}
@Override MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel);
public void onResume() { observePlayback();
super.onResume();
setMediaBrowserListenableFuture();
} }
@Override @Override
@ -254,9 +254,12 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null); songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
bind.songRecyclerView.setAdapter(songHorizontalAdapter); 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() { private void initializeMediaBrowser() {
@ -270,7 +273,6 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
@Override @Override
public void onMediaClick(Bundle bundle) { public void onMediaClick(Bundle bundle) {
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
songHorizontalAdapter.notifyDataSetChanged();
activity.setBottomSheetInPeek(true); activity.setBottomSheetInPeek(true);
} }
@ -279,9 +281,26 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); 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) { if (songHorizontalAdapter != null) {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); String id = playbackViewModel.getCurrentMediaId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
} }
} }
} }

View file

@ -34,6 +34,7 @@ import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter; import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.cappielloantonio.tempo.viewmodel.SearchViewModel; import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -46,6 +47,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
private FragmentSearchBinding bind; private FragmentSearchBinding bind;
private MainActivity activity; private MainActivity activity;
private SearchViewModel searchViewModel; private SearchViewModel searchViewModel;
private PlaybackViewModel playbackViewModel;
private ArtistAdapter artistAdapter; private ArtistAdapter artistAdapter;
private AlbumAdapter albumAdapter; private AlbumAdapter albumAdapter;
@ -61,6 +63,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
bind = FragmentSearchBinding.inflate(inflater, container, false); bind = FragmentSearchBinding.inflate(inflater, container, false);
View view = bind.getRoot(); View view = bind.getRoot();
searchViewModel = new ViewModelProvider(requireActivity()).get(SearchViewModel.class); searchViewModel = new ViewModelProvider(requireActivity()).get(SearchViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
initSearchResultView(); initSearchResultView();
initSearchView(); initSearchView();
@ -73,12 +76,14 @@ public class SearchFragment extends Fragment implements ClickCallback {
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
initializeMediaBrowser(); initializeMediaBrowser();
MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel);
observePlayback();
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
setMediaBrowserListenableFuture();
} }
@Override @Override
@ -119,7 +124,8 @@ public class SearchFragment extends Fragment implements ClickCallback {
bind.searchResultTracksRecyclerView.setHasFixedSize(true); bind.searchResultTracksRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null); songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
setMediaBrowserListenableFuture(); reapplyPlayback();
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter); bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
} }
@ -296,9 +302,26 @@ public class SearchFragment extends Fragment implements ClickCallback {
Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); 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) { if (songHorizontalAdapter != null) {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); String id = playbackViewModel.getCurrentMediaId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
} }
} }
} }

View file

@ -36,6 +36,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel; import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -49,6 +50,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
private FragmentSongListPageBinding bind; private FragmentSongListPageBinding bind;
private MainActivity activity; private MainActivity activity;
private SongListPageViewModel songListPageViewModel; private SongListPageViewModel songListPageViewModel;
private PlaybackViewModel playbackViewModel;
private SongHorizontalAdapter songHorizontalAdapter; private SongHorizontalAdapter songHorizontalAdapter;
@ -69,6 +71,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
bind = FragmentSongListPageBinding.inflate(inflater, container, false); bind = FragmentSongListPageBinding.inflate(inflater, container, false);
View view = bind.getRoot(); View view = bind.getRoot();
songListPageViewModel = new ViewModelProvider(requireActivity()).get(SongListPageViewModel.class); songListPageViewModel = new ViewModelProvider(requireActivity()).get(SongListPageViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
init(); init();
initAppBar(); initAppBar();
@ -82,12 +85,9 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
initializeMediaBrowser(); initializeMediaBrowser();
}
@Override MediaManager.registerPlaybackObserver(getViewLifecycleOwner(), mediaBrowserListenableFuture, playbackViewModel);
public void onResume() { observePlayback();
super.onResume();
setMediaBrowserListenableFuture();
} }
@Override @Override
@ -197,10 +197,11 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null); songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
bind.songListRecyclerView.setAdapter(songHorizontalAdapter); bind.songListRecyclerView.setAdapter(songHorizontalAdapter);
setMediaBrowserListenableFuture(); reapplyPlayback();
songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> { songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> {
isLoading = false; isLoading = false;
songHorizontalAdapter.setItems(songs); songHorizontalAdapter.setItems(songs);
reapplyPlayback();
setSongListPageSubtitle(songs); setSongListPageSubtitle(songs);
}); });
@ -325,7 +326,6 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
public void onMediaClick(Bundle bundle) { public void onMediaClick(Bundle bundle) {
hideKeyboard(requireView()); hideKeyboard(requireView());
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
songHorizontalAdapter.notifyDataSetChanged();
activity.setBottomSheetInPeek(true); activity.setBottomSheetInPeek(true);
} }
@ -334,9 +334,26 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); 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) { if (songHorizontalAdapter != null) {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); String id = playbackViewModel.getCurrentMediaId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
} }
} }
} }

View file

@ -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<String> currentMediaId = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> isPlaying = new MutableLiveData<>(false);
// (Optional) expose position or other info
// private final MutableLiveData<Long> positionMs = new MutableLiveData<>(0L);
public LiveData<String> getCurrentMediaId() {
return currentMediaId;
}
public LiveData<Boolean> 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);
}
}

View file

@ -66,16 +66,21 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" /> app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" />
<ImageView <ToggleButton
android:id="@+id/play_pause_icon" android:id="@+id/play_pause_button"
android:layout_width="28dp" android:layout_width="28dp"
android:layout_height="28dp" android:layout_height="28dp"
android:layout_marginStart="28dp" android:layout_marginStart="28dp"
android:visibility="gone" android:visibility="gone"
android:background="@drawable/button_play_pause_selector"
android:checked="false"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:text=""
android:textOff=""
android:textOn=""
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" />
android:src="@drawable/ic_play" />
<TextView <TextView
android:id="@+id/track_number_text_view" android:id="@+id/track_number_text_view"

View file

@ -31,16 +31,21 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ImageView <ToggleButton
android:id="@+id/play_pause_icon" android:id="@+id/play_pause_button"
android:layout_width="28dp" android:layout_width="28dp"
android:layout_height="28dp" android:layout_height="28dp"
android:layout_marginStart="14dp" android:layout_marginStart="14dp"
android:visibility="gone" android:visibility="gone"
android:background="@drawable/button_play_pause_selector"
android:checked="false"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:text=""
android:textOff=""
android:textOn=""
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent" />
android:src="@drawable/ic_play" />
<TextView <TextView
android:id="@+id/queue_song_title_text_view" android:id="@+id/queue_song_title_text_view"