diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/GridTrackAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/GridTrackAdapter.java new file mode 100644 index 00000000..0b604705 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/adapter/GridTrackAdapter.java @@ -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 { + private static final String TAG = "SimilarTrackAdapter"; + + private final MainActivity activity; + private final Context context; + private final LayoutInflater mInflater; + + private ListenableFuture mediaBrowserListenableFuture; + private List 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 items) { + this.items = items; + notifyDataSetChanged(); + } + + public void setMediaBrowserListenableFuture(ListenableFuture 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 = MappingUtil.mapChronology(items); + MediaManager.startQueue(mediaBrowserListenableFuture, context, media, getBindingAdapterPosition()); + activity.setBottomSheetInPeek(true); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java b/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java index 0bea82e5..7aa36bdb 100644 --- a/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java +++ b/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java @@ -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(); } diff --git a/app/src/main/java/com/cappielloantonio/play/database/dao/ChronologyDao.java b/app/src/main/java/com/cappielloantonio/play/database/dao/ChronologyDao.java new file mode 100644 index 00000000..52e3a110 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/database/dao/ChronologyDao.java @@ -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> getAllFrom(long startDate, long endDate); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(Chronology chronologyObject); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/helper/recyclerview/SquareLayout.java b/app/src/main/java/com/cappielloantonio/play/helper/recyclerview/SquareLayout.java new file mode 100644 index 00000000..2c319fd8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/helper/recyclerview/SquareLayout.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/model/Chronology.java b/app/src/main/java/com/cappielloantonio/play/model/Chronology.java new file mode 100644 index 00000000..f433a517 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/model/Chronology.java @@ -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; + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/model/Media.java b/app/src/main/java/com/cappielloantonio/play/model/Media.java index 480dec87..52447270 100644 --- a/app/src/main/java/com/cappielloantonio/play/model/Media.java +++ b/app/src/main/java/com/cappielloantonio/play/model/Media.java @@ -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; } diff --git a/app/src/main/java/com/cappielloantonio/play/repository/ChronologyRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/ChronologyRepository.java new file mode 100644 index 00000000..b4f8bab9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/repository/ChronologyRepository.java @@ -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> 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> 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); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/play/service/MediaManager.java index 72feabbb..ef491eb6 100644 --- a/app/src/main/java/com/cappielloantonio/play/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/play/service/MediaManager.java @@ -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, boolean reset, int afterIndex) { getQueueRepository().insertAll(media, reset, afterIndex); } diff --git a/app/src/main/java/com/cappielloantonio/play/service/MediaService.kt b/app/src/main/java/com/cappielloantonio/play/service/MediaService.kt index f6473997..58059f09 100644 --- a/app/src/main/java/com/cappielloantonio/play/service/MediaService.kt +++ b/app/src/main/java/com/cappielloantonio/play/service/MediaService.kt @@ -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 ): ListenableFuture> { 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() } diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/HomeFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/HomeFragment.java index 88ac10e0..207642d7 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/HomeFragment.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/HomeFragment.java @@ -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 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); } } diff --git a/app/src/main/java/com/cappielloantonio/play/util/MappingUtil.java b/app/src/main/java/com/cappielloantonio/play/util/MappingUtil.java index 9fc43e63..11950938 100644 --- a/app/src/main/java/com/cappielloantonio/play/util/MappingUtil.java +++ b/app/src/main/java/com/cappielloantonio/play/util/MappingUtil.java @@ -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 mapChronology(List items) { + ArrayList 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 mapPodcastChannel(List subsonicPodcastChannels) { ArrayList podcastChannels = new ArrayList(); diff --git a/app/src/main/java/com/cappielloantonio/play/viewmodel/HomeViewModel.java b/app/src/main/java/com/cappielloantonio/play/viewmodel/HomeViewModel.java index 6741d0d4..f0c0bd27 100644 --- a/app/src/main/java/com/cappielloantonio/play/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/cappielloantonio/play/viewmodel/HomeViewModel.java @@ -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> dicoverSongSample = new MutableLiveData<>(null); private final MutableLiveData> newReleasedAlbum = new MutableLiveData<>(null); @@ -48,6 +51,8 @@ public class HomeViewModel extends AndroidViewModel { private final MutableLiveData> pinnedPlaylists = new MutableLiveData<>(null); private final MutableLiveData> newestPodcastEpisodes = new MutableLiveData<>(null); + private final MutableLiveData> 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> getDiscoverSongSample(LifecycleOwner owner) { @@ -66,6 +72,21 @@ public class HomeViewModel extends AndroidViewModel { return dicoverSongSample; } + public LiveData> 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> getRecentlyReleasedAlbums(LifecycleOwner owner) { if (newReleasedAlbum.getValue() == null) { int currentYear = Calendar.getInstance().get(Calendar.YEAR); diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 1e50e2e1..4b88f988 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -171,6 +171,55 @@ android:layout_marginEnd="16dp" android:layout_marginBottom="12dp" /> + + + + + + + + + + + + + + + + \ No newline at end of file