Saving play history. The tracks are saved in the db at the time of playback and every week a list of the most played tracks is generated in the home page in grid format

This commit is contained in:
antonio 2022-12-28 14:25:59 +01:00
parent 6a1c5d2ce3
commit ff8bf4f6bf
14 changed files with 646 additions and 55 deletions

View file

@ -0,0 +1,98 @@
package com.cappielloantonio.play.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.media3.session.MediaBrowser;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.glide.CustomGlideRequest;
import com.cappielloantonio.play.model.Chronology;
import com.cappielloantonio.play.model.Media;
import com.cappielloantonio.play.service.MediaManager;
import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.util.MappingUtil;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
public class GridTrackAdapter extends RecyclerView.Adapter<GridTrackAdapter.ViewHolder> {
private static final String TAG = "SimilarTrackAdapter";
private final MainActivity activity;
private final Context context;
private final LayoutInflater mInflater;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private List<Chronology> items;
public GridTrackAdapter(MainActivity activity, Context context) {
this.activity = activity;
this.context = context;
this.mInflater = LayoutInflater.from(context);
this.items = new ArrayList<>();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = mInflater.inflate(R.layout.item_home_grid_track, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Chronology item = items.get(position);
CustomGlideRequest.Builder
.from(context, item.getCoverArtId(), CustomGlideRequest.SONG_PIC, null)
.build()
.transform(new CenterCrop(), new RoundedCorners(CustomGlideRequest.CORNER_RADIUS))
.into(holder.cover);
}
@Override
public int getItemCount() {
return items.size();
}
public Chronology getItem(int position) {
return items.get(position);
}
public void setItems(List<Chronology> items) {
this.items = items;
notifyDataSetChanged();
}
public void setMediaBrowserListenableFuture(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture) {
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
ImageView cover;
ViewHolder(View itemView) {
super(itemView);
cover = itemView.findViewById(R.id.track_cover_image_view);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
List<Media> media = MappingUtil.mapChronology(items);
MediaManager.startQueue(mediaBrowserListenableFuture, context, media, getBindingAdapterPosition());
activity.setBottomSheetInPeek(true);
}
}
}

View file

@ -8,11 +8,13 @@ import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import com.cappielloantonio.play.database.dao.ChronologyDao;
import com.cappielloantonio.play.database.dao.DownloadDao;
import com.cappielloantonio.play.database.dao.PlaylistDao;
import com.cappielloantonio.play.database.dao.QueueDao;
import com.cappielloantonio.play.database.dao.RecentSearchDao;
import com.cappielloantonio.play.database.dao.ServerDao;
import com.cappielloantonio.play.model.Chronology;
import com.cappielloantonio.play.model.Download;
import com.cappielloantonio.play.model.Playlist;
import com.cappielloantonio.play.model.Queue;
@ -21,9 +23,9 @@ import com.cappielloantonio.play.model.Server;
@SuppressLint("RestrictedApi")
@Database(
version = 41,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Playlist.class}
// autoMigrations = {@AutoMigration(from = 39, to = 40)}
version = 42,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Playlist.class, Chronology.class},
autoMigrations = {@AutoMigration(from = 41, to = 42)}
)
public abstract class AppDatabase extends RoomDatabase {
private static final String TAG = "AppDatabase";
@ -50,4 +52,6 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract DownloadDao downloadDao();
public abstract PlaylistDao playlistDao();
public abstract ChronologyDao chronologyDao();
}

View file

@ -0,0 +1,20 @@
package com.cappielloantonio.play.database.dao;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.cappielloantonio.play.model.Chronology;
import java.util.List;
@Dao
public interface ChronologyDao {
@Query("SELECT * FROM chronology WHERE timestamp >= :startDate AND timestamp < :endDate GROUP BY id ORDER BY COUNT(id) DESC LIMIT 9")
LiveData<List<Chronology>> getAllFrom(long startDate, long endDate);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(Chronology chronologyObject);
}

View file

@ -0,0 +1,28 @@
package com.cappielloantonio.play.helper.recyclerview;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
public class SquareLayout extends RelativeLayout {
public SquareLayout(Context context) {
super(context);
}
public SquareLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SquareLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SquareLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
}

View file

@ -0,0 +1,166 @@
package com.cappielloantonio.play.model;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "chronology")
public class Chronology {
@PrimaryKey(autoGenerate = true)
private int uuid;
@ColumnInfo(name = "id")
private String trackId;
@ColumnInfo(name = "title")
private String title;
@ColumnInfo(name = "albumId")
private String albumId;
@ColumnInfo(name = "albumName")
private String albumName;
@ColumnInfo(name = "artistId")
private String artistId;
@ColumnInfo(name = "artistName")
private String artistName;
@ColumnInfo(name = "cover_art_id")
private String coverArtId;
@ColumnInfo(name = "duration")
private long duration;
@ColumnInfo(name = "container")
private String container;
@ColumnInfo(name = "bitrate")
private int bitrate;
@ColumnInfo(name = "extension")
private String extension;
@ColumnInfo(name = "timestamp")
private Long timestamp;
public Chronology(String trackId, String title, String albumId, String albumName, String artistId, String artistName, String coverArtId, long duration, String container, int bitrate, String extension) {
this.trackId = trackId;
this.title = title;
this.albumId = albumId;
this.albumName = albumName;
this.artistId = artistId;
this.artistName = artistName;
this.coverArtId = coverArtId;
this.duration = duration;
this.container = container;
this.bitrate = bitrate;
this.extension = extension;
this.timestamp = System.currentTimeMillis();
}
public int getUuid() {
return uuid;
}
public void setUuid(int uuid) {
this.uuid = uuid;
}
public String getTrackId() {
return trackId;
}
public void setTrackId(String trackId) {
this.trackId = trackId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAlbumId() {
return albumId;
}
public void setAlbumId(String albumId) {
this.albumId = albumId;
}
public String getAlbumName() {
return albumName;
}
public void setAlbumName(String albumName) {
this.albumName = albumName;
}
public String getArtistId() {
return artistId;
}
public void setArtistId(String artistId) {
this.artistId = artistId;
}
public String getArtistName() {
return artistName;
}
public void setArtistName(String artistName) {
this.artistName = artistName;
}
public String getCoverArtId() {
return coverArtId;
}
public void setCoverArtId(String coverArtId) {
this.coverArtId = coverArtId;
}
public long getDuration() {
return duration;
}
public void setDuration(long duration) {
this.duration = duration;
}
public String getContainer() {
return container;
}
public void setContainer(String container) {
this.container = container;
}
public int getBitrate() {
return bitrate;
}
public void setBitrate(int bitrate) {
this.bitrate = bitrate;
}
public String getExtension() {
return extension;
}
public void setExtension(String extension) {
this.extension = extension;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
}

View file

@ -85,6 +85,7 @@ public class Media implements Parcelable {
this.playCount = 0;
this.lastPlay = 0;
this.rating = child.getUserRating() != null ? child.getUserRating() : 0;
// this.type = MEDIA_TYPE_MUSIC;
this.type = child.getType();
}
@ -106,6 +107,7 @@ public class Media implements Parcelable {
this.container = podcastEpisode.getContentType();
this.bitrate = podcastEpisode.getBitRate();
this.extension = podcastEpisode.getSuffix();
// this.type = MEDIA_TYPE_PODCAST;
this.type = podcastEpisode.getType();
}
@ -143,6 +145,21 @@ public class Media implements Parcelable {
this.type = download.getType();
}
public Media(Chronology item) {
this.id = item.getTrackId();
this.title = item.getTitle();
this.albumId = item.getAlbumId();
this.albumName = item.getAlbumName();
this.artistId = item.getArtistId();
this.artistName = item.getArtistName();
this.coverArtId = item.getCoverArtId();
this.duration = item.getDuration();
this.container = item.getContainer();
this.bitrate = item.getBitrate();
this.extension = item.getExtension();
this.type = MEDIA_TYPE_MUSIC;
}
public String getId() {
return id;
}

View file

@ -0,0 +1,68 @@
package com.cappielloantonio.play.repository;
import android.app.Application;
import androidx.lifecycle.LiveData;
import com.cappielloantonio.play.database.AppDatabase;
import com.cappielloantonio.play.database.dao.ChronologyDao;
import com.cappielloantonio.play.model.Chronology;
import java.util.Calendar;
import java.util.List;
public class ChronologyRepository {
private static final String TAG = "ChronologyRepository";
private final ChronologyDao chronologyDao;
public ChronologyRepository(Application application) {
AppDatabase database = AppDatabase.getInstance(application);
chronologyDao = database.chronologyDao();
}
public LiveData<List<Chronology>> getThisWeek() {
Calendar calendar = Calendar.getInstance();
Calendar first = (Calendar) calendar.clone();
first.add(Calendar.DAY_OF_WEEK, first.getFirstDayOfWeek() - first.get(Calendar.DAY_OF_WEEK));
Calendar last = (Calendar) first.clone();
last.add(Calendar.DAY_OF_YEAR, 6);
return chronologyDao.getAllFrom(first.getTime().getTime(), last.getTime().getTime());
}
public LiveData<List<Chronology>> getLastWeek() {
Calendar calendar = Calendar.getInstance();
Calendar first = (Calendar) calendar.clone();
first.add(Calendar.DAY_OF_WEEK, first.getFirstDayOfWeek() - first.get(Calendar.DAY_OF_WEEK) - 6);
Calendar last = (Calendar) first.clone();
last.add(Calendar.DAY_OF_YEAR, 6);
return chronologyDao.getAllFrom(first.getTime().getTime(), last.getTime().getTime());
}
public void insert(Chronology item) {
InsertThreadSafe insert = new InsertThreadSafe(chronologyDao, item);
Thread thread = new Thread(insert);
thread.start();
}
private static class InsertThreadSafe implements Runnable {
private final ChronologyDao chronologyDao;
private final Chronology item;
public InsertThreadSafe(ChronologyDao chronologyDao, Chronology item) {
this.chronologyDao = chronologyDao;
this.item = item;
}
@Override
public void run() {
chronologyDao.insert(item);
}
}
}

View file

@ -10,6 +10,7 @@ import androidx.media3.session.MediaBrowser;
import com.cappielloantonio.play.App;
import com.cappielloantonio.play.interfaces.MediaIndexCallback;
import com.cappielloantonio.play.model.Media;
import com.cappielloantonio.play.repository.ChronologyRepository;
import com.cappielloantonio.play.repository.QueueRepository;
import com.cappielloantonio.play.repository.SongRepository;
import com.cappielloantonio.play.util.MappingUtil;
@ -289,21 +290,22 @@ public class MediaManager {
}
}
@SuppressLint("UnsafeOptInUsageError")
public static void setLastPlayedTimestamp(MediaItem mediaItem) {
if (mediaItem != null) getQueueRepository().setLastPlayedTimestamp(mediaItem.mediaId);
}
@SuppressLint("UnsafeOptInUsageError")
public static void setPlayingPausedTimestamp(MediaItem mediaItem, long ms) {
if (mediaItem != null) getQueueRepository().setPlayingPausedTimestamp(mediaItem.mediaId, ms);
}
@SuppressLint("UnsafeOptInUsageError")
public static void scrobble(MediaItem mediaItem) {
if (mediaItem != null) getSongRepository().scrobble(mediaItem.mediaMetadata.extras.getString("id"));
}
public static void saveChronology(MediaItem mediaItem) {
if (mediaItem != null) getChronologyRepository().insert(MappingUtil.mapChronology(mediaItem));
}
private static QueueRepository getQueueRepository() {
return new QueueRepository(App.getInstance());
}
@ -312,6 +314,10 @@ public class MediaManager {
return new SongRepository(App.getInstance());
}
private static ChronologyRepository getChronologyRepository() {
return new ChronologyRepository(App.getInstance());
}
private static void enqueueDatabase(List<Media> media, boolean reset, int afterIndex) {
getQueueRepository().insertAll(media, reset, afterIndex);
}

View file

@ -9,20 +9,21 @@ import android.os.Bundle
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.*
import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.play.App
import com.cappielloantonio.play.R
import com.cappielloantonio.play.model.Media
import com.cappielloantonio.play.repository.QueueRepository
import com.cappielloantonio.play.ui.activity.MainActivity
import com.cappielloantonio.play.util.UIUtil
import com.google.android.gms.cast.framework.CastContext
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
@UnstableApi
class MediaService : MediaLibraryService(), SessionAvailabilityListener {
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
@ -49,7 +50,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
initializeMediaLibrarySession()
initializePlayerListener()
setPlayer(null, if (castPlayer.isCastSessionAvailable) castPlayer else player)
setPlayer(
null,
if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else player
)
}
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
@ -161,7 +165,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
controller: ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
val updatedMediaItems = mediaItems.map {
@ -198,8 +202,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
}
private fun initializeCastPlayer() {
castPlayer = CastPlayer(CastContext.getSharedInstance(this))
castPlayer.setSessionAvailabilityListener(this)
if (UIUtil.isCastApiAvailable(this)) {
castPlayer = CastPlayer(CastContext.getSharedInstance(this))
castPlayer.setSessionAvailabilityListener(this)
}
}
private fun initializeMediaLibrarySession() {
@ -224,8 +230,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (mediaItem == null) return
MediaManager.setLastPlayedTimestamp(mediaItem)
if (mediaItem.mediaMetadata.extras!!.getString("mediaType") == Media.MEDIA_TYPE_MUSIC)
if (mediaItem.mediaMetadata.extras!!.getString("mediaType") == Media.MEDIA_TYPE_MUSIC) {
MediaManager.scrobble(mediaItem)
MediaManager.saveChronology(mediaItem)
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
@ -246,8 +254,8 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
}
private fun releasePlayer() {
castPlayer.setSessionAvailabilityListener(null)
castPlayer.release()
if (this::castPlayer.isInitialized) castPlayer.setSessionAvailabilityListener(null)
if (this::castPlayer.isInitialized) castPlayer.release()
player.release()
mediaLibrarySession.release()
}

View file

@ -1,6 +1,5 @@
package com.cappielloantonio.play.ui.fragment;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -31,6 +30,7 @@ import com.cappielloantonio.play.adapter.AlbumHorizontalAdapter;
import com.cappielloantonio.play.adapter.ArtistAdapter;
import com.cappielloantonio.play.adapter.ArtistHorizontalAdapter;
import com.cappielloantonio.play.adapter.DiscoverSongAdapter;
import com.cappielloantonio.play.adapter.GridTrackAdapter;
import com.cappielloantonio.play.adapter.PodcastEpisodeAdapter;
import com.cappielloantonio.play.adapter.SimilarTrackAdapter;
import com.cappielloantonio.play.adapter.SongHorizontalAdapter;
@ -38,6 +38,7 @@ import com.cappielloantonio.play.adapter.YearAdapter;
import com.cappielloantonio.play.databinding.FragmentHomeBinding;
import com.cappielloantonio.play.helper.recyclerview.CustomLinearSnapHelper;
import com.cappielloantonio.play.helper.recyclerview.DotsIndicatorDecoration;
import com.cappielloantonio.play.helper.recyclerview.GridItemDecoration;
import com.cappielloantonio.play.model.Album;
import com.cappielloantonio.play.model.Artist;
import com.cappielloantonio.play.model.Media;
@ -71,6 +72,7 @@ public class HomeFragment extends Fragment {
private AlbumHorizontalAdapter newReleasesAlbumAdapter;
private YearAdapter yearAdapter;
private PodcastEpisodeAdapter podcastEpisodeAdapter;
private GridTrackAdapter gridTrackAdapter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@ -119,6 +121,7 @@ public class HomeFragment extends Fragment {
initRecentAddedAlbumView();
initPinnedPlaylistsView();
initNewestPodcastsView();
initGridView();
}
@Override
@ -258,11 +261,14 @@ public class HomeFragment extends Fragment {
bind.discoverSongViewPager.setOffscreenPageLimit(1);
homeViewModel.getDiscoverSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
if (songs == null) {
if (bind != null) bind.homeDiscoveryPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.homeDiscoveryPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.homeDiscoverSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeDiscoveryPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.homeDiscoverSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.homeDiscoveryPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.homeDiscoverSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE);
discoverSongAdapter.setItems(songs);
}
@ -279,11 +285,14 @@ public class HomeFragment extends Fragment {
bind.similarTracksRecyclerView.setAdapter(similarMusicAdapter);
homeViewModel.getStarredTracksSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
if (songs == null) {
if (bind != null) bind.homeSimilarTracksPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.homeSimilarTracksPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.homeSimilarTracksSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeSimilarTracksPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.homeSimilarTracksSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.homeSimilarTracksPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.homeSimilarTracksSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE);
similarMusicAdapter.setItems(songs);
}
@ -301,11 +310,14 @@ public class HomeFragment extends Fragment {
bind.radioArtistRecyclerView.setAdapter(radioArtistAdapter);
homeViewModel.getStarredArtistsSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> {
if (artists == null) {
if (bind != null) bind.homeRadioArtistPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.homeRadioArtistPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.homeRadioArtistSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeRadioArtistPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.homeRadioArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.homeRadioArtistPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.homeRadioArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE);
radioArtistAdapter.setItems(artists);
}
@ -322,11 +334,14 @@ public class HomeFragment extends Fragment {
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
if (songs == null) {
if (bind != null) bind.starredTracksPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.starredTracksPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.starredTracksSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.starredTracksPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.starredTracksSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.starredTracksPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.starredTracksSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.starredTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(songs.size(), 5), GridLayoutManager.HORIZONTAL, false));
@ -354,11 +369,14 @@ public class HomeFragment extends Fragment {
bind.starredAlbumsRecyclerView.setAdapter(starredAlbumAdapter);
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> {
if (albums == null) {
if (bind != null) bind.starredAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.starredAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.starredAlbumsSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.starredAlbumsPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.starredAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.starredAlbumsPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.starredAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.starredAlbumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(albums.size(), 5), GridLayoutManager.HORIZONTAL, false));
@ -386,11 +404,14 @@ public class HomeFragment extends Fragment {
bind.starredArtistsRecyclerView.setAdapter(starredArtistAdapter);
homeViewModel.getStarredArtists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> {
if (artists == null) {
if (bind != null) bind.starredArtistsPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.starredArtistsPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.starredArtistsSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.starredArtistsPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.starredArtistsSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.starredArtistsPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.starredArtistsSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.starredArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(artists.size(), 5), GridLayoutManager.HORIZONTAL, false));
@ -418,11 +439,14 @@ public class HomeFragment extends Fragment {
bind.newReleasesRecyclerView.setAdapter(newReleasesAlbumAdapter);
homeViewModel.getRecentlyReleasedAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> {
if (albums == null) {
if (bind != null) bind.homeNewReleasesPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.homeNewReleasesPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.homeNewReleasesSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeNewReleasesPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.homeNewReleasesSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.homeNewReleasesPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.homeNewReleasesSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.newReleasesRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(albums.size(), 5), GridLayoutManager.HORIZONTAL, false));
@ -457,11 +481,14 @@ public class HomeFragment extends Fragment {
bind.yearsRecyclerView.setAdapter(yearAdapter);
homeViewModel.getYearList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), years -> {
if (years == null) {
if (bind != null) bind.homeFlashbackPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.homeFlashbackPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.homeFlashbackSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeFlashbackPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.homeFlashbackSector.setVisibility(!years.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.homeFlashbackPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.homeFlashbackSector.setVisibility(!years.isEmpty() ? View.VISIBLE : View.GONE);
yearAdapter.setItems(years);
}
@ -479,12 +506,15 @@ public class HomeFragment extends Fragment {
bind.mostPlayedAlbumsRecyclerView.setAdapter(mostPlayedAlbumAdapter);
homeViewModel.getMostPlayedAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> {
if (albums == null) {
if (bind != null) bind.homeMostPlayedAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.homeMostPlayedAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.homeMostPlayedAlbumsSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeMostPlayedAlbumsPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.homeMostPlayedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
if (albums.size() < 10) reorder();
if (bind != null)
bind.homeMostPlayedAlbumsPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.homeMostPlayedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
if (albums.size() < 5) reorder();
mostPlayedAlbumAdapter.setItems(albums);
}
@ -502,11 +532,14 @@ public class HomeFragment extends Fragment {
bind.recentlyPlayedAlbumsRecyclerView.setAdapter(recentlyPlayedAlbumAdapter);
homeViewModel.getRecentlyPlayedAlbumList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> {
if (albums == null) {
if (bind != null) bind.homeRecentlyPlayedAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.homeRecentlyPlayedAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.homeRecentlyPlayedAlbumsSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeRecentlyPlayedAlbumsPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.homeRecentlyPlayedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.homeRecentlyPlayedAlbumsPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.homeRecentlyPlayedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
recentlyPlayedAlbumAdapter.setItems(albums);
}
@ -524,11 +557,14 @@ public class HomeFragment extends Fragment {
bind.recentlyAddedAlbumsRecyclerView.setAdapter(recentlyAddedAlbumAdapter);
homeViewModel.getMostRecentlyAddedAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> {
if (albums == null) {
if (bind != null) bind.homeRecentlyAddedAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null)
bind.homeRecentlyAddedAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.homeRecentlyAddedAlbumsSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeRecentlyAddedAlbumsPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.homeRecentlyAddedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.homeRecentlyAddedAlbumsPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.homeRecentlyAddedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
recentlyAddedAlbumAdapter.setItems(albums);
}
@ -606,7 +642,8 @@ public class HomeFragment extends Fragment {
if (podcastEpisodes == null) {
if (bind != null) bind.homeNewestPodcastsSector.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeNewestPodcastsSector.setVisibility(!podcastEpisodes.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.homeNewestPodcastsSector.setVisibility(!podcastEpisodes.isEmpty() ? View.VISIBLE : View.GONE);
podcastEpisodeAdapter.setItems(podcastEpisodes);
}
@ -615,6 +652,26 @@ public class HomeFragment extends Fragment {
setSlideViewOffset(bind.newestPodcastsViewPager, 20, 16);
}
private void initGridView() {
bind.gridTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3));
bind.gridTracksRecyclerView.addItemDecoration(new GridItemDecoration(3, 8, false));
bind.gridTracksRecyclerView.setHasFixedSize(true);
gridTrackAdapter = new GridTrackAdapter(activity, requireContext());
bind.gridTracksRecyclerView.setAdapter(gridTrackAdapter);
homeViewModel.getGridSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> {
if (chronologies == null || chronologies.size() == 0) {
if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE);
if (bind != null) bind.afterGridDivider.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeGridTracksSector.setVisibility(View.VISIBLE);
if (bind != null) bind.afterGridDivider.setVisibility(View.VISIBLE);
gridTrackAdapter.setItems(chronologies);
}
});
}
private void setSlideViewOffset(ViewPager2 viewPager, float pageOffset, float pageMargin) {
viewPager.setPageTransformer((page, position) -> {
float myOffset = position * -(2 * pageOffset + pageMargin);
@ -634,7 +691,12 @@ public class HomeFragment extends Fragment {
if (bind != null) {
bind.homeLinearLayoutContainer.removeAllViews();
bind.homeLinearLayoutContainer.addView(bind.homeDiscoverSector);
bind.homeLinearLayoutContainer.addView(bind.homeSimilarTracksSector);
// bind.homeLinearLayoutContainer.addView(bind.homeSimilarTracksSector);
// bind.homeLinearLayoutContainer.addView(bind.homeRadioArtistSector);
// bind.homeLinearLayoutContainer.addView(bind.homeGridTracksSector);
// bind.homeLinearLayoutContainer.addView(bind.starredTracksSector);
// bind.homeLinearLayoutContainer.addView(bind.starredAlbumsSector);
// bind.homeLinearLayoutContainer.addView(bind.starredArtistsSector);
bind.homeLinearLayoutContainer.addView(bind.homeRecentlyAddedAlbumsSector);
bind.homeLinearLayoutContainer.addView(bind.homeFlashbackSector);
bind.homeLinearLayoutContainer.addView(bind.homeMostPlayedAlbumsSector);
@ -643,7 +705,6 @@ public class HomeFragment extends Fragment {
}
}
@SuppressLint("UnsafeOptInUsageError")
private void initializeMediaBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
@ -658,5 +719,6 @@ public class HomeFragment extends Fragment {
starredSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
radioArtistAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
podcastEpisodeAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
gridTrackAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
}

View file

@ -1,16 +1,18 @@
package com.cappielloantonio.play.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.OptIn;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.play.model.Album;
import com.cappielloantonio.play.model.Artist;
import com.cappielloantonio.play.model.Chronology;
import com.cappielloantonio.play.model.Download;
import com.cappielloantonio.play.model.Media;
import com.cappielloantonio.play.model.Playlist;
@ -201,7 +203,7 @@ public class MappingUtil {
return genres;
}
@SuppressLint("UnsafeOptInUsageError")
@OptIn(markerClass = UnstableApi.class)
public static MediaItem mapMediaItem(Context context, Media media, boolean stream) {
boolean isDownloaded = DownloadUtil.getDownloadTracker(context).isDownloaded(MusicUtil.getDownloadUri(media.getId()));
@ -211,6 +213,7 @@ public class MappingUtil {
bundle.putString("artistId", media.getArtistId());
bundle.putString("coverArtId", media.getCoverArtId());
bundle.putString("mediaType", media.getType());
bundle.putLong("duration", media.getDuration());
bundle.putString("container", media.getContainer());
bundle.putInt("bitrate", media.getBitrate());
bundle.putString("extension", media.getExtension());
@ -268,6 +271,36 @@ public class MappingUtil {
return mediaItems;
}
public static Chronology mapChronology(MediaItem item) {
return new Chronology(
item.mediaId,
item.mediaMetadata.title.toString(),
item.mediaMetadata.extras.get("albumId").toString(),
item.mediaMetadata.albumTitle.toString(),
item.mediaMetadata.extras.get("artistId").toString(),
item.mediaMetadata.artist.toString(),
item.mediaMetadata.extras.get("coverArtId").toString(),
(long) item.mediaMetadata.extras.get("duration"),
item.mediaMetadata.extras.get("container").toString(),
(int) item.mediaMetadata.extras.get("bitrate"),
item.mediaMetadata.extras.get("extension").toString()
);
}
public static ArrayList<Media> mapChronology(List<Chronology> items) {
ArrayList<Media> songs = new ArrayList();
for (Chronology item : items) {
songs.add(mapSong(item));
}
return songs;
}
public static Media mapSong(Chronology item) {
return new Media(item);
}
public static ArrayList<PodcastChannel> mapPodcastChannel(List<com.cappielloantonio.play.subsonic.models.PodcastChannel> subsonicPodcastChannels) {
ArrayList<PodcastChannel> podcastChannels = new ArrayList();

View file

@ -11,10 +11,12 @@ import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.play.App;
import com.cappielloantonio.play.model.Album;
import com.cappielloantonio.play.model.Artist;
import com.cappielloantonio.play.model.Chronology;
import com.cappielloantonio.play.model.Media;
import com.cappielloantonio.play.model.Playlist;
import com.cappielloantonio.play.repository.AlbumRepository;
import com.cappielloantonio.play.repository.ArtistRepository;
import com.cappielloantonio.play.repository.ChronologyRepository;
import com.cappielloantonio.play.repository.PlaylistRepository;
import com.cappielloantonio.play.repository.PodcastRepository;
import com.cappielloantonio.play.repository.SongRepository;
@ -33,6 +35,7 @@ public class HomeViewModel extends AndroidViewModel {
private final ArtistRepository artistRepository;
private final PlaylistRepository playlistRepository;
private final PodcastRepository podcastRepository;
private final ChronologyRepository chronologyRepository;
private final MutableLiveData<List<Media>> dicoverSongSample = new MutableLiveData<>(null);
private final MutableLiveData<List<Album>> newReleasedAlbum = new MutableLiveData<>(null);
@ -48,6 +51,8 @@ public class HomeViewModel extends AndroidViewModel {
private final MutableLiveData<List<Playlist>> pinnedPlaylists = new MutableLiveData<>(null);
private final MutableLiveData<List<Media>> newestPodcastEpisodes = new MutableLiveData<>(null);
private final MutableLiveData<List<Chronology>> thisGridTopSong = new MutableLiveData<>(null);
public HomeViewModel(@NonNull Application application) {
super(application);
@ -56,6 +61,7 @@ public class HomeViewModel extends AndroidViewModel {
artistRepository = new ArtistRepository(application);
playlistRepository = new PlaylistRepository(application);
podcastRepository = new PodcastRepository(application);
chronologyRepository = new ChronologyRepository(application);
}
public LiveData<List<Media>> getDiscoverSongSample(LifecycleOwner owner) {
@ -66,6 +72,21 @@ public class HomeViewModel extends AndroidViewModel {
return dicoverSongSample;
}
public LiveData<List<Chronology>> getGridSongSample(LifecycleOwner owner) {
Calendar cal = Calendar.getInstance();
int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
if (thisGridTopSong.getValue() == null) {
if (dayOfMonth >= 7) {
chronologyRepository.getThisWeek().observe(owner, thisGridTopSong::postValue);
} else {
chronologyRepository.getLastWeek().observe(owner, thisGridTopSong::postValue);
}
}
return thisGridTopSong;
}
public LiveData<List<Album>> getRecentlyReleasedAlbums(LifecycleOwner owner) {
if (newReleasedAlbum.getValue() == null) {
int currentYear = Calendar.getInstance().get(Calendar.YEAR);

View file

@ -171,6 +171,55 @@
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp" />
<!-- Grid tracks -->
<LinearLayout
android:id="@+id/home_grid_tracks_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/grid_tracks_pre_text_view"
style="@style/TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="16dp"
android:text="Last week"
android:textAllCaps="true" />
<TextView
android:id="@+id/grid_tracks_text_view"
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="Your top songs" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/grid_tracks_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp" />
</LinearLayout>
<View
android:id="@+id/after_grid_divider"
style="@style/Divider"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp" />
<!-- Favorites -->
<LinearLayout
android:id="@+id/starred_tracks_sector"

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<com.cappielloantonio.play.helper.recyclerview.SquareLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/track_cover_image_view"
android:layout_width="156dp"
android:layout_height="156dp"
android:layout_gravity="center" />
</com.cappielloantonio.play.helper.recyclerview.SquareLayout>