diff --git a/.idea/misc.xml b/.idea/misc.xml index 2aa0a570..dbd49dc3 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -79,11 +79,17 @@ + + + + + + @@ -115,9 +121,12 @@ - + - + + + + diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/PlayerBodyAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/PlayerBodyAdapter.java new file mode 100644 index 00000000..ad5f3d17 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/adapter/PlayerBodyAdapter.java @@ -0,0 +1,34 @@ +package com.cappielloantonio.play.adapter; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import com.cappielloantonio.play.ui.fragment.PlayerControllerFragment; +import com.cappielloantonio.play.ui.fragment.PlayerQueueFragment; + +public class PlayerBodyAdapter extends FragmentStateAdapter { + private static final String TAG = "PlayerNowPlayingSongInfoAdapter"; + + public PlayerBodyAdapter(@NonNull Fragment fragment) { + super(fragment); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + switch (position) { + case 0: + return new PlayerControllerFragment(); + case 1: + return new PlayerQueueFragment(); + } + + return new PlayerControllerFragment(); + } + + @Override + public int getItemCount() { + return 2; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/PlayerSongQueueAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/PlayerSongQueueAdapter.java index ee387276..8feb8a37 100644 --- a/app/src/main/java/com/cappielloantonio/play/adapter/PlayerSongQueueAdapter.java +++ b/app/src/main/java/com/cappielloantonio/play/adapter/PlayerSongQueueAdapter.java @@ -27,15 +27,13 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter mediaBrowserListenableFuture; private List songs; - public PlayerSongQueueAdapter(Context context, PlayerBottomSheetFragment playerBottomSheetFragment) { + public PlayerSongQueueAdapter(Context context) { this.context = context; - this.playerBottomSheetFragment = playerBottomSheetFragment; this.mInflater = LayoutInflater.from(context); this.songs = new ArrayList<>(); } diff --git a/app/src/main/java/com/cappielloantonio/play/helper/recyclerview/NestedScrollableHost.kt b/app/src/main/java/com/cappielloantonio/play/helper/recyclerview/NestedScrollableHost.kt new file mode 100644 index 00000000..caacffe3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/helper/recyclerview/NestedScrollableHost.kt @@ -0,0 +1,88 @@ +package com.cappielloantonio.play.helper.recyclerview + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.widget.FrameLayout +import androidx.viewpager2.widget.ViewPager2 +import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL +import kotlin.math.absoluteValue +import kotlin.math.sign + +class NestedScrollableHost : FrameLayout { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + private var touchSlop = 0 + private var initialX = 0f + private var initialY = 0f + private val parentViewPager: ViewPager2? + get() { + var v: View? = parent as? View + while (v != null && v !is ViewPager2) { + v = v.parent as? View + } + return v as? ViewPager2 + } + + private val child: View? get() = if (childCount > 0) getChildAt(0) else null + + init { + touchSlop = ViewConfiguration.get(context).scaledTouchSlop + } + + private fun canChildScroll(orientation: Int, delta: Float): Boolean { + val direction = -delta.sign.toInt() + return when (orientation) { + 0 -> child?.canScrollHorizontally(direction) ?: false + 1 -> child?.canScrollVertically(direction) ?: false + else -> throw IllegalArgumentException() + } + } + + override fun onInterceptTouchEvent(e: MotionEvent): Boolean { + handleInterceptTouchEvent(e) + return super.onInterceptTouchEvent(e) + } + + private fun handleInterceptTouchEvent(e: MotionEvent) { + val orientation = parentViewPager?.orientation ?: return + + // Early return if child can't scroll in same direction as parent + if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) { + return + } + + if (e.action == MotionEvent.ACTION_DOWN) { + initialX = e.x + initialY = e.y + parent.requestDisallowInterceptTouchEvent(true) + } else if (e.action == MotionEvent.ACTION_MOVE) { + val dx = e.x - initialX + val dy = e.y - initialY + val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL + + // assuming ViewPager2 touch-slop is 2x touch-slop of child + val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f + val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f + + if (scaledDx > touchSlop || scaledDy > touchSlop) { + if (isVpHorizontal == (scaledDy > scaledDx)) { + // Gesture is perpendicular, allow all parents to intercept + parent.requestDisallowInterceptTouchEvent(false) + } else { + // Gesture is parallel, query child if movement in that direction is possible + if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) { + // Child can scroll, disallow all parents to intercept + parent.requestDisallowInterceptTouchEvent(true) + } else { + // Child cannot scroll, allow all parents to intercept + parent.requestDisallowInterceptTouchEvent(false) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/ui/activity/MainActivity.java b/app/src/main/java/com/cappielloantonio/play/ui/activity/MainActivity.java index 5307602f..44578381 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/activity/MainActivity.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/activity/MainActivity.java @@ -152,18 +152,18 @@ public class MainActivity extends BaseActivity { break; case BottomSheetBehavior.STATE_COLLAPSED: if (playerBottomSheetFragment != null) { - playerBottomSheetFragment.goBackToFirstPage(); - playerBottomSheetFragment.scrollOnTop(); + // playerBottomSheetFragment.goBackToFirstPage(); + // playerBottomSheetFragment.scrollOnTop(); } case BottomSheetBehavior.STATE_SETTLING: if (playerBottomSheetFragment != null) { - playerBottomSheetFragment.scrollOnTop(); + // playerBottomSheetFragment.scrollOnTop(); } break; case BottomSheetBehavior.STATE_EXPANDED: if (playerBottomSheetFragment != null) { - playerBottomSheetFragment.scrollOnTop(); - setBottomSheetDraggableState(playerBottomSheetFragment.isViewPagerInFirstPage()); + // playerBottomSheetFragment.scrollOnTop(); + // setBottomSheetDraggableState(playerBottomSheetFragment.isViewPagerInFirstPage()); } case BottomSheetBehavior.STATE_DRAGGING: case BottomSheetBehavior.STATE_HALF_EXPANDED: diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerBottomSheetFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerBottomSheetFragment.java index ac9441b7..96c58cf9 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerBottomSheetFragment.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerBottomSheetFragment.java @@ -23,6 +23,7 @@ import androidx.media3.common.Player; import androidx.media3.session.MediaBrowser; import androidx.media3.session.MediaController; import androidx.media3.session.SessionToken; +import androidx.media3.ui.PlayerControlView; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; @@ -31,6 +32,7 @@ import androidx.viewpager2.widget.ViewPager2; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.cappielloantonio.play.R; +import com.cappielloantonio.play.adapter.PlayerBodyAdapter; import com.cappielloantonio.play.adapter.PlayerNowPlayingSongAdapter; import com.cappielloantonio.play.adapter.PlayerSongQueueAdapter; import com.cappielloantonio.play.databinding.FragmentPlayerBottomSheetBinding; @@ -51,20 +53,11 @@ public class PlayerBottomSheetFragment extends Fragment { private static final String TAG = "PlayerBottomSheetFragment"; private FragmentPlayerBottomSheetBinding bind; - private ImageView playerMoveDownBottomSheet; - private ViewPager2 playerSongCoverViewPager; - private RecyclerView playerQueueRecyclerView; - private ToggleButton buttonFavorite; - private ImageButton playerCommandUnfoldButton; - private CardView playerCommandCardview; - private TextView playerSongTitleLabel; - private TextView playerArtistNameLabel; - private MainActivity activity; private PlayerBottomSheetViewModel playerBottomSheetViewModel; private ListenableFuture mediaBrowserListenableFuture; - private PlayerSongQueueAdapter playerSongQueueAdapter; + // TODO: Collegare la seekbar all'exo_progress @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -74,19 +67,12 @@ public class PlayerBottomSheetFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - activity = (MainActivity) getActivity(); - bind = FragmentPlayerBottomSheetBinding.inflate(inflater, container, false); View view = bind.getRoot(); playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); - init(); - initCoverLyricsSlideView(); - initQueueRecyclerView(); - initMediaListenable(); - initMusicCommandUnfoldButton(); - initArtistLabelButton(); + initViewPager(); return view; } @@ -97,7 +83,6 @@ public class PlayerBottomSheetFragment extends Fragment { initializeMediaBrowser(); bindMediaController(); - setMediaBrowserListenableFuture(); } @Override @@ -112,19 +97,9 @@ public class PlayerBottomSheetFragment extends Fragment { bind = null; } - @SuppressLint("UnsafeOptInUsageError") - private void init() { - playerMoveDownBottomSheet = bind.getRoot().findViewById(R.id.player_move_down_bottom_sheet); - playerSongCoverViewPager = bind.getRoot().findViewById(R.id.player_song_cover_view_pager); - playerQueueRecyclerView = bind.getRoot().findViewById(R.id.player_queue_recycler_view); - buttonFavorite = bind.getRoot().findViewById(R.id.button_favorite); - playerCommandUnfoldButton = bind.getRoot().findViewById(R.id.player_command_unfold_button); - playerCommandCardview = bind.getRoot().findViewById(R.id.player_command_cardview); - playerSongTitleLabel = bind.getRoot().findViewById(R.id.player_song_title_label); - playerArtistNameLabel = bind.getRoot().findViewById(R.id.player_artist_name_label); - - playerMoveDownBottomSheet.setOnClickListener(view -> activity.collapseBottomSheet()); - bind.playerBodyLayout.setProgressUpdateListener((position, bufferedPosition) -> bind.playerHeaderLayout.playerHeaderSeekBar.setProgress((int) (position / 1000), true)); + private void initViewPager() { + bind.playerBodyLayout.playerBodyBottomSheetViewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL); + bind.playerBodyLayout.playerBodyBottomSheetViewPager.setAdapter(new PlayerBodyAdapter(this)); } @SuppressLint("UnsafeOptInUsageError") @@ -136,18 +111,12 @@ public class PlayerBottomSheetFragment extends Fragment { MediaController.releaseFuture(mediaBrowserListenableFuture); } - private void setMediaBrowserListenableFuture() { - playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); - } - @SuppressLint("UnsafeOptInUsageError") private void bindMediaController() { mediaBrowserListenableFuture.addListener(() -> { try { MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); - bind.playerBodyLayout.setPlayer(mediaBrowser); - setMediaControllerListener(mediaBrowser); } catch (Exception e) { Log.e(TAG, e.getMessage()); @@ -190,9 +159,6 @@ public class PlayerBottomSheetFragment extends Fragment { bind.playerHeaderLayout.playerHeaderSongTitleLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.title))); bind.playerHeaderLayout.playerHeaderSongArtistLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.artist))); - playerSongTitleLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.title))); - playerArtistNameLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.artist))); - if (mediaMetadata.extras != null) CustomGlideRequest.Builder .from(requireContext(), mediaMetadata.extras.getString("id"), CustomGlideRequest.SONG_PIC, null) .build() @@ -226,143 +192,7 @@ public class PlayerBottomSheetFragment extends Fragment { bind.playerHeaderLayout.playerHeaderNextSongButton.setAlpha(isEnabled ? (float) 1.0 : (float) 0.3); } - private void initCoverLyricsSlideView() { - playerSongCoverViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL); - playerSongCoverViewPager.setAdapter(new PlayerNowPlayingSongAdapter(this)); - - playerSongCoverViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageSelected(int position) { - super.onPageSelected(position); - - if (position == 0) { - activity.setBottomSheetDraggableState(true); - } else if (position == 1) { - activity.setBottomSheetDraggableState(false); - } - } - }); - } - - private void initQueueRecyclerView() { - playerQueueRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); - playerQueueRecyclerView.setHasFixedSize(true); - - playerSongQueueAdapter = new PlayerSongQueueAdapter(requireContext(), this); - playerQueueRecyclerView.setAdapter(playerSongQueueAdapter); - playerBottomSheetViewModel.getQueueSong().observe(requireActivity(), queue -> { - if (queue != null) { - playerSongQueueAdapter.setItems(MappingUtil.mapQueue(queue)); - } - }); - - new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT) { - int originalPosition = -1; - int fromPosition = -1; - int toPosition = -1; - - @Override - public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { - if (originalPosition == -1) { - originalPosition = viewHolder.getBindingAdapterPosition(); - } - - fromPosition = viewHolder.getBindingAdapterPosition(); - toPosition = target.getBindingAdapterPosition(); - - /* - * Per spostare un elemento nella coda devo: - * - Spostare graficamente la traccia da una posizione all'altra con Collections.swap() - * - Spostare nel db la traccia, tramite QueueRepository - * - Notificare il Service dell'avvenuto spostamento con MusicPlayerRemote.moveSong() - * - * In onMove prendo la posizione di inizio e fine, ma solo al rilascio dell'elemento procedo allo spostamento - * In questo modo evito che ad ogni cambio di posizione vada a riscrivere nel db - * Al rilascio dell'elemento chiamo il metodo clearView() - */ - - Collections.swap(playerSongQueueAdapter.getItems(), fromPosition, toPosition); - recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition); - - return false; - } - - @Override - public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - - if (originalPosition != -1 && fromPosition != -1 && toPosition != -1) { - MediaManager.swap(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), originalPosition, toPosition); - } - - originalPosition = -1; - fromPosition = -1; - toPosition = -1; - } - - @Override - public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { - MediaManager.remove(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), viewHolder.getBindingAdapterPosition()); - viewHolder.getBindingAdapter().notifyDataSetChanged(); - } - }).attachToRecyclerView(playerQueueRecyclerView); - } - - private void initMediaListenable() { - playerBottomSheetViewModel.getLiveSong().observe(requireActivity(), song -> { - if (song != null) { - buttonFavorite.setChecked(song.isFavorite()); - - buttonFavorite.setOnClickListener(v -> playerBottomSheetViewModel.setFavorite(requireContext(), song)); - - buttonFavorite.setOnLongClickListener(v -> { - Bundle bundle = new Bundle(); - bundle.putParcelable("song_object", song); - - RatingDialog dialog = new RatingDialog(); - dialog.setArguments(bundle); - dialog.show(requireActivity().getSupportFragmentManager(), null); - - return true; - }); - - playerBottomSheetViewModel.refreshSongInfo(requireActivity(), song); - } - }); - } - - private void initMusicCommandUnfoldButton() { - playerCommandUnfoldButton.setOnClickListener(view -> { - if (playerCommandCardview.getVisibility() == View.INVISIBLE || playerCommandCardview.getVisibility() == View.GONE) { - playerCommandCardview.setVisibility(View.VISIBLE); - } else { - playerCommandCardview.setVisibility(View.GONE); - } - }); - } - - private void initArtistLabelButton() { - playerArtistNameLabel.setOnClickListener(view -> playerBottomSheetViewModel.getLiveArtist().observe(requireActivity(), artist -> { - Bundle bundle = new Bundle(); - bundle.putParcelable("artist_object", artist); - NavHostFragment.findNavController(this).navigate(R.id.artistPageFragment, bundle); - activity.collapseBottomSheet(); - })); - } - public View getPlayerHeader() { return requireView().findViewById(R.id.player_header_layout); } - - public void scrollOnTop() { - bind.playerNestedScrollView.fullScroll(ScrollView.FOCUS_UP); - } - - public void goBackToFirstPage() { - playerSongCoverViewPager.setCurrentItem(0); - } - - public boolean isViewPagerInFirstPage() { - return playerSongCoverViewPager.getCurrentItem() == 0; - } } diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerControllerFragment.java new file mode 100644 index 00000000..e4230760 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerControllerFragment.java @@ -0,0 +1,206 @@ +package com.cappielloantonio.play.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.ToggleButton; + +import androidx.annotation.NonNull; +import androidx.cardview.widget.CardView; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.Player; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.fragment.NavHostFragment; +import androidx.viewpager2.widget.ViewPager2; + +import com.cappielloantonio.play.R; +import com.cappielloantonio.play.adapter.PlayerNowPlayingSongAdapter; +import com.cappielloantonio.play.databinding.InnerFragmentPlayerControllerBinding; +import com.cappielloantonio.play.service.MediaService; +import com.cappielloantonio.play.ui.activity.MainActivity; +import com.cappielloantonio.play.ui.dialog.RatingDialog; +import com.cappielloantonio.play.util.MusicUtil; +import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +public class PlayerControllerFragment extends Fragment { + private static final String TAG = "PlayerCoverFragment"; + + private InnerFragmentPlayerControllerBinding bind; + private ImageView playerMoveDownBottomSheet; + private ViewPager2 playerSongCoverViewPager; + private ToggleButton buttonFavorite; + private ImageButton playerCommandUnfoldButton; + private CardView playerCommandCardview; + private TextView playerSongTitleLabel; + private TextView playerArtistNameLabel; + + private MainActivity activity; + private PlayerBottomSheetViewModel playerBottomSheetViewModel; + private ListenableFuture mediaBrowserListenableFuture; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = InnerFragmentPlayerControllerBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); + + init(); + initCoverLyricsSlideView(); + initMediaListenable(); + initMusicCommandUnfoldButton(); + initArtistLabelButton(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + initializeBrowser(); + bindMediaController(); + } + + @Override + public void onStop() { + releaseBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @SuppressLint("UnsafeOptInUsageError") + private void init() { + playerMoveDownBottomSheet = bind.getRoot().findViewById(R.id.player_move_down_bottom_sheet); + playerSongCoverViewPager = bind.getRoot().findViewById(R.id.player_song_cover_view_pager); + buttonFavorite = bind.getRoot().findViewById(R.id.button_favorite); + playerCommandUnfoldButton = bind.getRoot().findViewById(R.id.player_command_unfold_button); + playerCommandCardview = bind.getRoot().findViewById(R.id.player_command_cardview); + playerSongTitleLabel = bind.getRoot().findViewById(R.id.player_song_title_label); + playerArtistNameLabel = bind.getRoot().findViewById(R.id.player_artist_name_label); + + playerMoveDownBottomSheet.setOnClickListener(view -> activity.collapseBottomSheet()); + } + + @SuppressLint("UnsafeOptInUsageError") + private void initializeBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @SuppressLint("UnsafeOptInUsageError") + private void bindMediaController() { + mediaBrowserListenableFuture.addListener(() -> { + try { + MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + + bind.playerControlBodyLayout.setPlayer(mediaBrowser); + + setMediaControllerListener(mediaBrowser); + } catch (Exception e) { + Log.e(TAG, e.getMessage()); + } + }, MoreExecutors.directExecutor()); + } + + @SuppressLint("UnsafeOptInUsageError") + private void setMediaControllerListener(MediaBrowser mediaBrowser) { + setMetadata(mediaBrowser.getMediaMetadata()); + + mediaBrowser.addListener(new Player.Listener() { + @Override + public void onMediaMetadataChanged(@NonNull MediaMetadata mediaMetadata) { + setMetadata(mediaMetadata); + } + }); + } + + private void setMetadata(MediaMetadata mediaMetadata) { + if (mediaMetadata.extras != null) playerBottomSheetViewModel.setLiveSong(requireActivity(), mediaMetadata.extras.getString("id")); + if (mediaMetadata.extras != null) playerBottomSheetViewModel.setLiveArtist(requireActivity(), mediaMetadata.extras.getString("artistId")); + + playerSongTitleLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.title))); + playerArtistNameLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.artist))); + } + + private void initCoverLyricsSlideView() { + playerSongCoverViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL); + playerSongCoverViewPager.setAdapter(new PlayerNowPlayingSongAdapter(this)); + + playerSongCoverViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + + if (position == 0) { + activity.setBottomSheetDraggableState(true); + } else if (position == 1) { + activity.setBottomSheetDraggableState(false); + } + } + }); + } + + private void initMediaListenable() { + playerBottomSheetViewModel.getLiveSong().observe(requireActivity(), song -> { + if (song != null) { + buttonFavorite.setChecked(song.isFavorite()); + + buttonFavorite.setOnClickListener(v -> playerBottomSheetViewModel.setFavorite(requireContext(), song)); + + buttonFavorite.setOnLongClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable("song_object", song); + + RatingDialog dialog = new RatingDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + + return true; + }); + + playerBottomSheetViewModel.refreshSongInfo(requireActivity(), song); + } + }); + } + + private void initMusicCommandUnfoldButton() { + playerCommandUnfoldButton.setOnClickListener(view -> { + if (playerCommandCardview.getVisibility() == View.INVISIBLE || playerCommandCardview.getVisibility() == View.GONE) { + playerCommandCardview.setVisibility(View.VISIBLE); + } else { + playerCommandCardview.setVisibility(View.GONE); + } + }); + } + + private void initArtistLabelButton() { + playerArtistNameLabel.setOnClickListener(view -> playerBottomSheetViewModel.getLiveArtist().observe(requireActivity(), artist -> { + Bundle bundle = new Bundle(); + bundle.putParcelable("artist_object", artist); + NavHostFragment.findNavController(this).navigate(R.id.artistPageFragment, bundle); + activity.collapseBottomSheet(); + })); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerCoverFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerCoverFragment.java index 7e710a90..b56061bd 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerCoverFragment.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerCoverFragment.java @@ -10,36 +10,28 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; import androidx.media3.session.MediaBrowser; import androidx.media3.session.SessionToken; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; -import com.cappielloantonio.play.databinding.FragmentPlayerCoverBinding; +import com.cappielloantonio.play.databinding.InnerFragmentPlayerCoverBinding; import com.cappielloantonio.play.glide.CustomGlideRequest; import com.cappielloantonio.play.service.MediaService; -import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; public class PlayerCoverFragment extends Fragment { private static final String TAG = "PlayerCoverFragment"; - private FragmentPlayerCoverBinding bind; - private PlayerBottomSheetViewModel playerBottomSheetViewModel; - + private InnerFragmentPlayerCoverBinding bind; private ListenableFuture mediaBrowserListenableFuture; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - bind = FragmentPlayerCoverBinding.inflate(inflater, container, false); - View view = bind.getRoot(); - - playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); - - return view; + bind = InnerFragmentPlayerCoverBinding.inflate(inflater, container, false); + return bind.getRoot(); } @Override diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerLyricsFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerLyricsFragment.java index f65f9126..6fdbf30d 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerLyricsFragment.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerLyricsFragment.java @@ -11,19 +11,19 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.cappielloantonio.play.R; -import com.cappielloantonio.play.databinding.FragmentPlayerLyricsBinding; +import com.cappielloantonio.play.databinding.InnerFragmentPlayerLyricsBinding; import com.cappielloantonio.play.util.MusicUtil; import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel; public class PlayerLyricsFragment extends Fragment { private static final String TAG = "PlayerLyricsFragment"; - private FragmentPlayerLyricsBinding bind; + private InnerFragmentPlayerLyricsBinding bind; private PlayerBottomSheetViewModel playerBottomSheetViewModel; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - bind = FragmentPlayerLyricsBinding.inflate(inflater, container, false); + bind = InnerFragmentPlayerLyricsBinding.inflate(inflater, container, false); View view = bind.getRoot(); playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerQueueFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerQueueFragment.java new file mode 100644 index 00000000..c0fea4c8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerQueueFragment.java @@ -0,0 +1,157 @@ +package com.cappielloantonio.play.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.play.adapter.PlayerSongQueueAdapter; +import com.cappielloantonio.play.databinding.InnerFragmentPlayerQueueBinding; +import com.cappielloantonio.play.service.MediaManager; +import com.cappielloantonio.play.service.MediaService; +import com.cappielloantonio.play.util.MappingUtil; +import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.Collections; + +public class PlayerQueueFragment extends Fragment { + private static final String TAG = "PlayerCoverFragment"; + + private InnerFragmentPlayerQueueBinding bind; + + private PlayerBottomSheetViewModel playerBottomSheetViewModel; + private ListenableFuture mediaBrowserListenableFuture; + + private PlayerSongQueueAdapter playerSongQueueAdapter; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + bind = InnerFragmentPlayerQueueBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); + + init(); + initQueueRecyclerView(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + initializeBrowser(); + } + + @Override + public void onResume() { + super.onResume(); + setMediaBrowserListenableFuture(); + } + + @Override + public void onStop() { + releaseBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @SuppressLint("UnsafeOptInUsageError") + private void init() { + + } + + @SuppressLint("UnsafeOptInUsageError") + private void initializeBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + private void setMediaBrowserListenableFuture() { + playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + } + + private void initQueueRecyclerView() { + bind.playerQueueRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.playerQueueRecyclerView.setHasFixedSize(true); + + playerSongQueueAdapter = new PlayerSongQueueAdapter(requireContext()); + bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter); + playerBottomSheetViewModel.getQueueSong().observe(requireActivity(), queue -> { + if (queue != null) { + playerSongQueueAdapter.setItems(MappingUtil.mapQueue(queue)); + } + }); + + new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT) { + int originalPosition = -1; + int fromPosition = -1; + int toPosition = -1; + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { + if (originalPosition == -1) { + originalPosition = viewHolder.getBindingAdapterPosition(); + } + + fromPosition = viewHolder.getBindingAdapterPosition(); + toPosition = target.getBindingAdapterPosition(); + + /* + * Per spostare un elemento nella coda devo: + * - Spostare graficamente la traccia da una posizione all'altra con Collections.swap() + * - Spostare nel db la traccia, tramite QueueRepository + * - Notificare il Service dell'avvenuto spostamento con MusicPlayerRemote.moveSong() + * + * In onMove prendo la posizione di inizio e fine, ma solo al rilascio dell'elemento procedo allo spostamento + * In questo modo evito che ad ogni cambio di posizione vada a riscrivere nel db + * Al rilascio dell'elemento chiamo il metodo clearView() + */ + + Collections.swap(playerSongQueueAdapter.getItems(), fromPosition, toPosition); + recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition); + + return false; + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + + if (originalPosition != -1 && fromPosition != -1 && toPosition != -1) { + MediaManager.swap(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), originalPosition, toPosition); + } + + originalPosition = -1; + fromPosition = -1; + toPosition = -1; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + MediaManager.remove(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), viewHolder.getBindingAdapterPosition()); + viewHolder.getBindingAdapter().notifyDataSetChanged(); + } + }).attachToRecyclerView(bind.playerQueueRecyclerView); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_player_bottom_sheet.xml b/app/src/main/res/layout/fragment_player_bottom_sheet.xml index 44c5a0a2..1e46c503 100644 --- a/app/src/main/res/layout/fragment_player_bottom_sheet.xml +++ b/app/src/main/res/layout/fragment_player_bottom_sheet.xml @@ -1,33 +1,18 @@ - - - + android:layout_height="match_parent" /> - - - - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/inner_fragment_player_controller.xml b/app/src/main/res/layout/inner_fragment_player_controller.xml new file mode 100644 index 00000000..0355c830 --- /dev/null +++ b/app/src/main/res/layout/inner_fragment_player_controller.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_player_cover.xml b/app/src/main/res/layout/inner_fragment_player_cover.xml similarity index 100% rename from app/src/main/res/layout/fragment_player_cover.xml rename to app/src/main/res/layout/inner_fragment_player_cover.xml diff --git a/app/src/main/res/layout/fragment_player_lyrics.xml b/app/src/main/res/layout/inner_fragment_player_lyrics.xml similarity index 100% rename from app/src/main/res/layout/fragment_player_lyrics.xml rename to app/src/main/res/layout/inner_fragment_player_lyrics.xml diff --git a/app/src/main/res/layout/inner_fragment_player_queue.xml b/app/src/main/res/layout/inner_fragment_player_queue.xml new file mode 100644 index 00000000..9e8a0cc0 --- /dev/null +++ b/app/src/main/res/layout/inner_fragment_player_queue.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_body_bottom_sheet.xml b/app/src/main/res/layout/player_body_bottom_sheet.xml index 9a95b1c5..c3a60af6 100644 --- a/app/src/main/res/layout/player_body_bottom_sheet.xml +++ b/app/src/main/res/layout/player_body_bottom_sheet.xml @@ -1,259 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + android:background="@color/colorPrimary" /> \ No newline at end of file diff --git a/app/src/main/res/layout/player_control_view_body.xml b/app/src/main/res/layout/player_control_view_body.xml new file mode 100644 index 00000000..6883a300 --- /dev/null +++ b/app/src/main/res/layout/player_control_view_body.xml @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_control_view_header.xml b/app/src/main/res/layout/player_control_view_header.xml new file mode 100644 index 00000000..f093237e --- /dev/null +++ b/app/src/main/res/layout/player_control_view_header.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file