diff --git a/app/build.gradle b/app/build.gradle index 98948d6e..9e40c569 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,6 +30,7 @@ android { } buildFeatures { + dataBinding true viewBinding = true } } @@ -75,6 +76,9 @@ dependencies { implementation 'com.github.bumptech.glide:glide:4.11.0' implementation "com.github.woltapp:blurhash:f41a23cc50" + // Exoplayer + implementation 'com.google.android.exoplayer:exoplayer:2.12.2' + annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' annotationProcessor "androidx.room:room-compiler:2.2.5" testImplementation 'junit:junit:4.13.1' diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/PlayerNowPlayingSongAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/PlayerNowPlayingSongAdapter.java index bee305cd..d3095448 100644 --- a/app/src/main/java/com/cappielloantonio/play/adapter/PlayerNowPlayingSongAdapter.java +++ b/app/src/main/java/com/cappielloantonio/play/adapter/PlayerNowPlayingSongAdapter.java @@ -69,6 +69,14 @@ public class PlayerNowPlayingSongAdapter extends RecyclerView.Adapter songs) { this.songs = songs; notifyDataSetChanged(); 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 159dda72..96a0d26a 100644 --- a/app/src/main/java/com/cappielloantonio/play/adapter/PlayerSongQueueAdapter.java +++ b/app/src/main/java/com/cappielloantonio/play/adapter/PlayerSongQueueAdapter.java @@ -7,16 +7,14 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; import com.cappielloantonio.play.App; import com.cappielloantonio.play.R; import com.cappielloantonio.play.glide.CustomGlideRequest; import com.cappielloantonio.play.model.Song; -import com.cappielloantonio.play.repository.QueueRepository; import com.cappielloantonio.play.repository.SongRepository; -import com.cappielloantonio.play.ui.activities.MainActivity; +import com.cappielloantonio.play.ui.fragment.PlayerBottomSheetFragment; import java.util.ArrayList; import java.util.List; @@ -29,14 +27,12 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter songs; private LayoutInflater mInflater; - private MainActivity mainActivity; + private PlayerBottomSheetFragment playerBottomSheetFragment; private Context context; - private FragmentManager fragmentManager; - public PlayerSongQueueAdapter(MainActivity mainActivity, Context context, FragmentManager fragmentManager) { - this.mainActivity = mainActivity; + public PlayerSongQueueAdapter(Context context, PlayerBottomSheetFragment playerBottomSheetFragment) { this.context = context; - this.fragmentManager = fragmentManager; + this.playerBottomSheetFragment = playerBottomSheetFragment; this.mInflater = LayoutInflater.from(context); this.songs = new ArrayList<>(); } @@ -83,9 +79,9 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter> getAll(); - @Query("SELECT * FROM song JOIN queue ON song.id = queue.song_id") + @Query("SELECT * FROM song JOIN queue ON song.id = queue.id") List getAllSimple(); + @Query("SELECT * FROM song JOIN queue ON song.id = queue.id WHERE queue.rowid = :position") + Song getSongByIndex(int position); + + @Query("SELECT * FROM song JOIN queue ON song.id = queue.id WHERE queue.last_played != 0 ORDER BY queue.last_played DESC LIMIT 1") + LiveData getLastPlayedSong(); + + @Query("UPDATE queue SET last_played = :timestamp WHERE queue.rowid = :position") + void setLastPlayedSong(int position, long timestamp); + + @Query("UPDATE queue SET last_played = :timestamp WHERE queue.id = :songID") + void setLastPlayedSong(String songID, long timestamp); + @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(Queue songQueueObject); diff --git a/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java b/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java index 170e6d42..294c66d5 100644 --- a/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java +++ b/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java @@ -51,12 +51,15 @@ public interface SongDao { @Query("SELECT * FROM song INNER Join song_genre_cross ON song.id = song_genre_cross.song_id AND song_genre_cross.genre_id IN (:filters) GROUP BY song.id") LiveData> getFilteredSong(ArrayList filters); - @Query("SELECT * FROM song WHERE favorite = 1 ORDER BY play_count DESC LIMIT :number") + @Query("SELECT * FROM song WHERE favorite = 1 LIMIT :number") LiveData> getFavoriteSongSample(int number); - @Query("SELECT * FROM song WHERE favorite = 1 ORDER BY play_count DESC") + @Query("SELECT * FROM song WHERE favorite = 1") LiveData> getFavoriteSong(); + @Query("SELECT * FROM song WHERE id = :id") + Song getSongByID(String id); + @Query("SELECT EXISTS(SELECT * FROM song WHERE id = :id)") boolean exist(String id); diff --git a/app/src/main/java/com/cappielloantonio/play/glide/CustomGlideRequest.java b/app/src/main/java/com/cappielloantonio/play/glide/CustomGlideRequest.java index c0f67d02..7a631df0 100644 --- a/app/src/main/java/com/cappielloantonio/play/glide/CustomGlideRequest.java +++ b/app/src/main/java/com/cappielloantonio/play/glide/CustomGlideRequest.java @@ -74,7 +74,7 @@ public class CustomGlideRequest { public static String createUrl(String item, String itemType, String quality) { ImageOptions options = new ImageOptions(); - switch(itemType) { + switch (itemType) { case PRIMARY: { options.setImageType(ImageType.Primary); break; @@ -85,7 +85,7 @@ public class CustomGlideRequest { } } - switch(quality) { + switch (quality) { case TOP_QUALITY: { options.setQuality(100); options.setMaxHeight(800); diff --git a/app/src/main/java/com/cappielloantonio/play/model/DirectPlayCodec.java b/app/src/main/java/com/cappielloantonio/play/model/DirectPlayCodec.java new file mode 100644 index 00000000..e7b102f6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/model/DirectPlayCodec.java @@ -0,0 +1,31 @@ +package com.cappielloantonio.play.model; + +public class DirectPlayCodec { + public Codec codec; + public boolean selected; + + public DirectPlayCodec(Codec codec, boolean selected) { + this.codec = codec; + this.selected = selected; + } + + public enum Codec { + FLAC("FLAC", "FLAC", "flac|flac"), + MP3("MP3", "MP3", "mp3|mp3"), + OPUS("Opus", "Opus", "opus|opus"), + AAC("M4A", "AAC", "m4a|aac"), + VORBIS("OGG", "Vorbis", "ogg|vorbis"), + OGG("OGG", "Opus", "ogg|opus"), + MKA("MKA", "Opus", "mka|opus"); + + public final String container; + public final String codec; + public final String value; + + Codec(String container, String codec, String value) { + this.container = container; + this.codec = codec; + this.value = value; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/model/Queue.java b/app/src/main/java/com/cappielloantonio/play/model/Queue.java index 8396c344..5cbf4451 100644 --- a/app/src/main/java/com/cappielloantonio/play/model/Queue.java +++ b/app/src/main/java/com/cappielloantonio/play/model/Queue.java @@ -3,35 +3,21 @@ package com.cappielloantonio.play.model; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; -import androidx.room.Ignore; import androidx.room.PrimaryKey; @Entity(tableName = "queue") public class Queue { @NonNull - @PrimaryKey(autoGenerate = true) + @PrimaryKey @ColumnInfo(name = "id") - private int id; - - @ColumnInfo(name = "song_id") private String songID; - public Queue(@NonNull int id, String songID) { - this.id = id; + @ColumnInfo(name = "last_played") + private long lastPlayed; + + public Queue(String songID, long lastPlayed) { this.songID = songID; - } - - @Ignore - public Queue(String songID) { - this.songID = songID; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; + this.lastPlayed = lastPlayed; } public String getSongID() { @@ -41,4 +27,12 @@ public class Queue { public void setSongID(String songID) { this.songID = songID; } + + public long getLastPlayed() { + return lastPlayed; + } + + public void setLastPlayed(long lastPlayed) { + this.lastPlayed = lastPlayed; + } } diff --git a/app/src/main/java/com/cappielloantonio/play/repository/QueueRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/QueueRepository.java index 1c57f61a..cb45b9db 100644 --- a/app/src/main/java/com/cappielloantonio/play/repository/QueueRepository.java +++ b/app/src/main/java/com/cappielloantonio/play/repository/QueueRepository.java @@ -10,6 +10,7 @@ import com.cappielloantonio.play.model.Queue; import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.util.QueueUtil; +import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -18,6 +19,7 @@ public class QueueRepository { private QueueDao queueDao; private LiveData> listLiveQueue; + private LiveData liveLastPlayedSong; public QueueRepository(Application application) { AppDatabase database = AppDatabase.getInstance(application); @@ -29,6 +31,34 @@ public class QueueRepository { return listLiveQueue; } + public Song getSongByPosition(int position) { + Song song = null; + + GetSongByPositionThreadSafe getSong = new GetSongByPositionThreadSafe(queueDao, position); + Thread thread = new Thread(getSong); + thread.start(); + + try { + thread.join(); + song = getSong.getSong(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return song; + } + + public LiveData getLiveLastPlayedSong() { + liveLastPlayedSong = queueDao.getLastPlayedSong(); + return liveLastPlayedSong; + } + + public void setLiveLastPlayedSong(Song song, int position) { + SetLastPlayedSongThreadSafe update = new SetLastPlayedSongThreadSafe(queueDao, song, position); + Thread thread = new Thread(update); + thread.start(); + } + public List getSongs() { List songs = new ArrayList<>(); @@ -194,4 +224,44 @@ public class QueueRepository { return songs; } } + + private static class SetLastPlayedSongThreadSafe implements Runnable { + private QueueDao queueDao; + private Song song; + private int position; + + public SetLastPlayedSongThreadSafe(QueueDao queueDao, Song song, int position) { + this.queueDao = queueDao; + this.song = song; + this.position = position; + } + + @Override + public void run() { + if(song != null) + queueDao.setLastPlayedSong(song.getId(), Instant.now().toEpochMilli()); + else + queueDao.setLastPlayedSong(position, Instant.now().toEpochMilli()); + } + } + + private static class GetSongByPositionThreadSafe implements Runnable { + private QueueDao queueDao; + private int position; + private Song song; + + public GetSongByPositionThreadSafe(QueueDao queueDao, int position) { + this.queueDao = queueDao; + this.position = position; + } + + @Override + public void run() { + song = queueDao.getSongByIndex(position); + } + + public Song getSong() { + return song; + } + } } diff --git a/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java index fb40212e..aa517e1c 100644 --- a/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java @@ -246,6 +246,23 @@ public class SongRepository { return sample; } + public Song getSongByID(String id) { + Song song = null; + + GetSongByIDThreadSafe songByID = new GetSongByIDThreadSafe(songDao, id); + Thread thread = new Thread(songByID); + thread.start(); + + try { + thread.join(); + song = songByID.getSong(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return song; + } + private static class ExistThreadSafe implements Runnable { private SongDao songDao; private Song song; @@ -459,4 +476,24 @@ public class SongRepository { return decades; } } + + private static class GetSongByIDThreadSafe implements Runnable { + private SongDao songDao; + private String id; + private Song song; + + public GetSongByIDThreadSafe(SongDao songDao, String id) { + this.songDao = songDao; + this.id = id; + } + + @Override + public void run() { + song = songDao.getSongByID(id); + } + + public Song getSong() { + return song; + } + } } diff --git a/app/src/main/java/com/cappielloantonio/play/service/MusicService.java b/app/src/main/java/com/cappielloantonio/play/service/MusicService.java new file mode 100644 index 00000000..15e3434f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/service/MusicService.java @@ -0,0 +1,97 @@ +package com.cappielloantonio.play.service; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; +import android.media.AudioManager; +import android.net.Uri; +import android.os.IBinder; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import com.cappielloantonio.play.R; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.util.Util; + +public class MusicService extends Service implements AudioManager.OnAudioFocusChangeListener { + private AudioManager audioManager; + private MediaSource mediaSource; + private SimpleExoPlayer player; + + @Override + public void onCreate() { + audioManager = (AudioManager) getApplicationContext().getSystemService(AUDIO_SERVICE); + super.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + String input = intent.getStringExtra("playStop"); + if (input != null && input.equals("play")) { + if (requestFocus()) { + initPlayer(); + play(); + } + } else { + stop(); + } + startForeground(1, getNotification()); + return START_NOT_STICKY; + } + + @Override + public void onDestroy() { + stop(); + super.onDestroy(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private Notification getNotification() { + NotificationCompat.Builder builder = new NotificationCompat.Builder(this) + .setContentText("Running ...") + .setContentTitle("Play") + .setOngoing(true) + .setSmallIcon(R.drawable.exo_controls_shuffle_on) + .setChannelId("PlayApp"); + return builder.build(); + } + + @Override + public void onAudioFocusChange(int focusChange) { + if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS) { + stop(); + System.exit(0); + } + } + + private boolean requestFocus() { + return (audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + } + + private void play() { + player.setForegroundMode(true); + player.prepare(mediaSource); + player.setPlayWhenReady(true); + } + + private void stop() { + player.setPlayWhenReady(false); + player.stop(); + } + + private void initPlayer() { + player = ExoPlayerFactory.newSimpleInstance(getApplicationContext()); + DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(this, Util.getUserAgent(getApplicationContext(), "exoPlayerSample")); + mediaSource = new ProgressiveMediaSource.Factory(defaultDataSourceFactory).createMediaSource(Uri.parse("http://192.168.1.81:8096/Audio/5656e9fd11e38ba95ba4871bc061991a/universal?UserId=34addd030b4545e5ac4300dc322c9f73&DeviceId=e40853e4e7ab76f1&MaxStreamingBitrate=10000000&Container=flac|flac,mp3|mp3,opus|opus,m4a|aac,ogg|vorbis,ogg|opus,mka|opus&TranscodingContainer=ts&TranscodingProtocol=hls&AudioCodec=aac&api_key=7e6626ca220d4b01961022e148868d41")); + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java b/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java index 6ccde626..8ddc16a9 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java @@ -3,7 +3,6 @@ package com.cappielloantonio.play.ui.activities; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.os.Bundle; -import android.util.Log; import android.view.View; import androidx.annotation.NonNull; @@ -17,6 +16,7 @@ import com.cappielloantonio.play.App; import com.cappielloantonio.play.R; import com.cappielloantonio.play.broadcast.receiver.ConnectivityStatusBroadcastReceiver; import com.cappielloantonio.play.databinding.ActivityMainBinding; +import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.ui.activities.base.BaseActivity; import com.cappielloantonio.play.ui.fragment.PlayerBottomSheetFragment; import com.cappielloantonio.play.util.PreferenceUtil; @@ -81,42 +81,6 @@ public class MainActivity extends BaseActivity { } } - private void initBottomSheet() { - bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.player_bottom_sheet)); - bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback); - fragmentManager.beginTransaction().replace(R.id.player_bottom_sheet, new PlayerBottomSheetFragment(), "PlayerBottomSheet").commit(); - - isBottomSheetInPeek(mainViewModel.isQueueLoaded()); - } - - public void isBottomSheetInPeek(Boolean isVisible) { - - Log.d(TAG, "isBottomSheetInPeek: " + isVisible); - - if (isVisible) { - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } else { - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } - } - - private void initNavigation() { - bottomNavigationView = findViewById(R.id.bottom_navigation); - navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.nav_host_fragment); - navController = navHostFragment.getNavController(); - navController.addOnDestinationChangedListener((controller, destination, arguments) -> { - if(bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED && ( - destination.getId() == R.id.homeFragment || - destination.getId() == R.id.libraryFragment || - destination.getId() == R.id.searchFragment || - destination.getId() == R.id.settingsFragment) - ) { - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } - }); - NavigationUI.setupWithNavController(bottomNavigationView, navController); - } - private void checkPreviousSession() { App.getApiClientInstance(getApplicationContext()).ChangeServerLocation(PreferenceUtil.getInstance(this).getServer()); App.getApiClientInstance(getApplicationContext()).SetAuthenticationInfo(PreferenceUtil.getInstance(this).getToken(), PreferenceUtil.getInstance(this).getUser()); @@ -140,6 +104,49 @@ public class MainActivity extends BaseActivity { }); } + + // BOTTOM SHEET/NAVIGATION + private void initBottomSheet() { + bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.player_bottom_sheet)); + bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback); + fragmentManager.beginTransaction().replace(R.id.player_bottom_sheet, new PlayerBottomSheetFragment(), "PlayerBottomSheet").commit(); + + /* + * All'apertura mostro il bottom sheet solo se in coda c'è qualcosa + */ + isBottomSheetInPeek(mainViewModel.isQueueLoaded()); + } + + public void isBottomSheetInPeek(Boolean isVisible) { + if (isVisible) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } else { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + } + + private void initNavigation() { + bottomNavigationView = findViewById(R.id.bottom_navigation); + navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.nav_host_fragment); + navController = navHostFragment.getNavController(); + + /* + * In questo modo intercetto il cambio schermata tramite navbar e se il bottom sheet è aperto, + * lo chiudo + */ + navController.addOnDestinationChangedListener((controller, destination, arguments) -> { + if(bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED && ( + destination.getId() == R.id.homeFragment || + destination.getId() == R.id.libraryFragment || + destination.getId() == R.id.searchFragment || + destination.getId() == R.id.settingsFragment) + ) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + NavigationUI.setupWithNavController(bottomNavigationView, navController); + } + public void setBottomNavigationBarVisibility(boolean visibility) { if (visibility) { bottomNavigationView.setVisibility(View.VISIBLE); @@ -161,7 +168,7 @@ public class MainActivity extends BaseActivity { @Override public void onStateChanged(@NonNull View view, int state) { switch (state) { - case BottomSheetBehavior.STATE_COLLAPSED: + case BottomSheetBehavior.STATE_SETTLING: PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet"); if(playerBottomSheetFragment == null) break; @@ -184,6 +191,18 @@ public class MainActivity extends BaseActivity { } }; + /* + * Scroll on top del bottom sheet quando chiudo + * In questo modo non mi ritrovo al posto dell'header una parte centrale del player + */ + public void setBottomSheetMusicInfo(Song song) { + PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet"); + if(playerBottomSheetFragment == null) return; + + playerBottomSheetFragment.scrollPager(song, 0, false); + } + + // NAVIGATION public void goToLogin() { if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment) navController.navigate(R.id.action_landingFragment_to_loginFragment); @@ -225,6 +244,15 @@ public class MainActivity extends BaseActivity { } } + @Override + public void onBackPressed() { + if(bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + else + super.onBackPressed(); + } + + // CONNECTION private void connectivityStatusReceiverManager(boolean isActive) { if (isActive) { IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); @@ -233,12 +261,4 @@ public class MainActivity extends BaseActivity { unregisterReceiver(connectivityStatusBroadcastReceiver); } } - - @Override - public void onBackPressed() { - if(bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - else - super.onBackPressed(); - } } \ No newline at end of file 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 da5f2c41..ca6f9fcf 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 @@ -5,10 +5,10 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ScrollView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.view.ViewCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; @@ -20,6 +20,7 @@ import com.cappielloantonio.play.adapter.PlayerSongQueueAdapter; import com.cappielloantonio.play.databinding.FragmentPlayerBottomSheetBinding; import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.ui.activities.MainActivity; +import com.cappielloantonio.play.util.MusicUtil; import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel; public class PlayerBottomSheetFragment extends Fragment { @@ -32,6 +33,8 @@ public class PlayerBottomSheetFragment extends Fragment { private PlayerNowPlayingSongAdapter playerNowPlayingSongAdapter; private PlayerSongQueueAdapter playerSongQueueAdapter; + private boolean isNowPlaying = false; + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -43,64 +46,59 @@ public class PlayerBottomSheetFragment extends Fragment { initQueueSlideView(); initQueueRecyclerView(); + initFavoriteButtonClick(); return view; } private void initQueueSlideView() { - bind.playerSongCoverViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL); + bind.playerBodyLayout.playerSongCoverViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL); playerNowPlayingSongAdapter = new PlayerNowPlayingSongAdapter(requireContext()); - bind.playerSongCoverViewPager.setAdapter(playerNowPlayingSongAdapter); + bind.playerBodyLayout.playerSongCoverViewPager.setAdapter(playerNowPlayingSongAdapter); playerBottomSheetViewModel.getQueueSong().observe(requireActivity(), songs -> playerNowPlayingSongAdapter.setItems(songs)); - bind.playerSongCoverViewPager.setOffscreenPageLimit(3); - setDiscoverSongSlideViewOffset(40, 4); - - bind.playerSongCoverViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + bind.playerBodyLayout.playerSongCoverViewPager.setOffscreenPageLimit(3); + bind.playerBodyLayout.playerSongCoverViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - super.onPageScrolled(position, positionOffset, positionOffsetPixels); + public void onPageSelected(int position) { + super.onPageSelected(position); - playerBottomSheetViewModel.setNowPlayingSong(position); + Song song = playerNowPlayingSongAdapter.getItem(position); + if (song != null && song != playerBottomSheetViewModel.getSong()) setSongInfo(song); } }); - - playerBottomSheetViewModel.getNowPlayingSong().observe(requireActivity(), song -> { - if(song != null) - setSongInfo(song); - }); } private void initQueueRecyclerView() { - bind.playerQueueRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); - bind.playerQueueRecyclerView.setHasFixedSize(true); + bind.playerBodyLayout.playerQueueRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.playerBodyLayout.playerQueueRecyclerView.setHasFixedSize(true); - playerSongQueueAdapter = new PlayerSongQueueAdapter(activity, requireContext(), getChildFragmentManager()); - bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter); + playerSongQueueAdapter = new PlayerSongQueueAdapter(requireContext(), this); + bind.playerBodyLayout.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter); playerBottomSheetViewModel.getQueueSong().observe(requireActivity(), songs -> playerSongQueueAdapter.setItems(songs)); } - private void setDiscoverSongSlideViewOffset(float pageOffset, float pageMargin) { - bind.playerSongCoverViewPager.setPageTransformer((page, position) -> { - float myOffset = position * -(2 * pageOffset + pageMargin); - if (bind.playerSongCoverViewPager.getOrientation() == ViewPager2.ORIENTATION_HORIZONTAL) { - if (ViewCompat.getLayoutDirection(bind.playerSongCoverViewPager) == ViewCompat.LAYOUT_DIRECTION_RTL) { - page.setTranslationX(-myOffset); - } else { - page.setTranslationX(myOffset); - } - } else { - page.setTranslationY(myOffset); - } - }); + private void initFavoriteButtonClick() { + bind.playerBodyLayout.buttonFavorite.setOnClickListener(v -> playerBottomSheetViewModel.setFavorite()); } private void setSongInfo(Song song) { - if(song != null) { - bind.playerSongTitleLabel.setText(song.getTitle()); - bind.playerArtistNameLabel.setText(song.getArtistName()); - } + playerBottomSheetViewModel.setNowPlayingSong(song); + + bind.playerBodyLayout.playerSongTitleLabel.setText(song.getTitle()); + bind.playerBodyLayout.playerArtistNameLabel.setText(song.getArtistName()); + + bind.playerHeaderLayout.playerHeaderSongTitleLabel.setText(song.getTitle()); + bind.playerHeaderLayout.playerHeaderSongArtistLabel.setText(song.getArtistName()); + + bind.playerBodyLayout.buttonFavorite.setChecked(song.isFavorite()); + + playSong(song); + } + + private void playSong(Song song) { + Toast.makeText(activity, MusicUtil.getSongFileUri(song), Toast.LENGTH_SHORT).show(); } public View getPlayerHeader() { @@ -110,4 +108,9 @@ public class PlayerBottomSheetFragment extends Fragment { public void scrollOnTop() { bind.playerNestedScrollView.fullScroll(ScrollView.FOCUS_UP); } + + public void scrollPager(Song song, int page, boolean smoothScroll) { + bind.playerBodyLayout.playerSongCoverViewPager.setCurrentItem(page, smoothScroll); + setSongInfo(song); + } } diff --git a/app/src/main/java/com/cappielloantonio/play/util/MusicUtil.java b/app/src/main/java/com/cappielloantonio/play/util/MusicUtil.java new file mode 100644 index 00000000..ad8a6701 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/util/MusicUtil.java @@ -0,0 +1,54 @@ +package com.cappielloantonio.play.util; + +import android.util.Log; + +import com.cappielloantonio.play.App; +import com.cappielloantonio.play.model.DirectPlayCodec; +import com.cappielloantonio.play.model.Song; + +import org.jellyfin.apiclient.interaction.ApiClient; + +public class MusicUtil { + public static String getSongFileUri(Song song) { + ApiClient apiClient = App.getApiClientInstance(App.getInstance()); + PreferenceUtil preferenceUtil = PreferenceUtil.getInstance(App.getInstance()); + + StringBuilder builder = new StringBuilder(256); + builder.append(apiClient.getApiUrl()); + builder.append("/Audio/"); + builder.append(song.getId()); + builder.append("/universal"); + builder.append("?UserId=").append(apiClient.getCurrentUserId()); + builder.append("&DeviceId=").append(apiClient.getDeviceId()); + + // web client maximum is 12444445 and 320kbps is 320000 + builder.append("&MaxStreamingBitrate=").append(preferenceUtil.getMaximumBitrate()); + + boolean containerAdded = false; + for (DirectPlayCodec directPlayCodec : preferenceUtil.getDirectPlayCodecs()) { + if (directPlayCodec.selected) { + if (!containerAdded) { + builder.append("&Container="); + containerAdded = true; + } + + builder.append(directPlayCodec.codec.value).append(','); + } + } + + if (containerAdded) { + // remove last comma + builder.deleteCharAt(builder.length() - 1); + } + + builder.append("&TranscodingContainer=ts"); + builder.append("&TranscodingProtocol=hls"); + + // preferred codec when transcoding + builder.append("&AudioCodec=").append(preferenceUtil.getTranscodeCodec()); + builder.append("&api_key=").append(apiClient.getAccessToken()); + + Log.i(MusicUtil.class.getName(), "playing audio: " + builder); + return builder.toString(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java b/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java index 7a96cc08..c101408a 100644 --- a/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java +++ b/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java @@ -6,6 +6,12 @@ import android.content.SharedPreferences; import androidx.preference.PreferenceManager; import com.cappielloantonio.play.helper.ThemeHelper; +import com.cappielloantonio.play.model.DirectPlayCodec; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; public class PreferenceUtil { public static final String SERVER = "server"; @@ -19,6 +25,10 @@ public class PreferenceUtil { public static final String HOST_URL = "host"; public static final String IMAGE_CACHE_SIZE = "image_cache_size"; + public static final String TRANSCODE_CODEC = "transcode_codec"; + public static final String DIRECT_PLAY_CODECS = "direct_play_codecs"; + public static final String MAXIMUM_BITRATE = "maximum_bitrate"; + private static PreferenceUtil sInstance; private final SharedPreferences mPreferences; @@ -104,4 +114,46 @@ public class PreferenceUtil { public final int getImageCacheSize() { return Integer.parseInt(mPreferences.getString(IMAGE_CACHE_SIZE, "400000000")); } + + public final String getTranscodeCodec() { + return mPreferences.getString(TRANSCODE_CODEC, "aac"); + } + + public final String getMaximumBitrate() { + return mPreferences.getString(MAXIMUM_BITRATE, "10000000"); + } + + public List getDirectPlayCodecs() { + DirectPlayCodec.Codec[] codecs = DirectPlayCodec.Codec.values(); + + Set selectedCodecNames = new HashSet<>(); + for (DirectPlayCodec.Codec codec : codecs) { + // this will be the default value + selectedCodecNames.add(codec.name()); + } + + selectedCodecNames = mPreferences.getStringSet(DIRECT_PLAY_CODECS, selectedCodecNames); + + ArrayList directPlayCodecs = new ArrayList<>(); + for (DirectPlayCodec.Codec codec : codecs) { + String name = codec.name(); + boolean selected = selectedCodecNames.contains(name); + directPlayCodecs.add(new DirectPlayCodec(codec, selected)); + } + + return directPlayCodecs; + } + + public void setDirectPlayCodecs(List directPlayCodecs) { + Set codecNames = new HashSet<>(); + for (DirectPlayCodec directPlayCodec : directPlayCodecs) { + if (directPlayCodec.selected) { + codecNames.add(directPlayCodec.codec.toString()); + } + } + + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putStringSet(DIRECT_PLAY_CODECS, codecNames); + editor.apply(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/util/QueueUtil.java b/app/src/main/java/com/cappielloantonio/play/util/QueueUtil.java index 66b825de..53cb9430 100644 --- a/app/src/main/java/com/cappielloantonio/play/util/QueueUtil.java +++ b/app/src/main/java/com/cappielloantonio/play/util/QueueUtil.java @@ -8,14 +8,14 @@ import java.util.List; public class QueueUtil { public static Queue getQueueElementFromSong(Song song) { - return new Queue(song.getId()); + return new Queue(song.getId(), 0); } public static List getQueueElementsFromSongs(List songs) { List queue = new ArrayList<>(); for(Song song: songs) { - queue.add(new Queue(song.getId())); + queue.add(new Queue(song.getId(), 0)); } return queue; diff --git a/app/src/main/java/com/cappielloantonio/play/viewmodel/PlayerBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/play/viewmodel/PlayerBottomSheetViewModel.java index f76099ed..c51e41ea 100644 --- a/app/src/main/java/com/cappielloantonio/play/viewmodel/PlayerBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/play/viewmodel/PlayerBottomSheetViewModel.java @@ -5,24 +5,25 @@ import android.app.Application; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.repository.QueueRepository; +import com.cappielloantonio.play.repository.SongRepository; import java.util.List; public class PlayerBottomSheetViewModel extends AndroidViewModel { - private static final String TAG = "HomeViewModel"; + private static final String TAG = "PlayerBottomSheetViewModel"; + private SongRepository songRepository; private QueueRepository queueRepository; private LiveData> queueSong; - private LiveData nowPlayingSong = new MutableLiveData<>(); - + private Song song; public PlayerBottomSheetViewModel(@NonNull Application application) { super(application); + songRepository = new SongRepository(application); queueRepository = new QueueRepository(application); queueSong = queueRepository.getLiveQueue(); @@ -32,17 +33,24 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { return queueSong; } - public LiveData getNowPlayingSong() { - return nowPlayingSong; + public void setNowPlayingSong(Song song) { + this.song = song; } - public LiveData setNowPlayingSong(int position) { - Song song = queueRepository.getSongs().get(position); + public void setFavorite() { + if(song.isFavorite()) + song.setFavorite(false); + else + song.setFavorite(true); - if(song != null) { - nowPlayingSong = new MutableLiveData<>(song); - } + songRepository.setFavoriteStatus(song); + } - return nowPlayingSong; + public Song getSong() { + return song; + } + + public void setSong(Song song) { + this.song = song; } } 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 e8ab9a46..3cd95fbd 100644 --- a/app/src/main/res/layout/fragment_player_bottom_sheet.xml +++ b/app/src/main/res/layout/fragment_player_bottom_sheet.xml @@ -21,112 +21,13 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toTopOf="parent"/> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index c9ef3d98..530ec2da 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -99,15 +99,15 @@ android:id="@+id/search_result_nested_scroll_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="gone" - android:paddingBottom="@dimen/global_padding_bottom"> + android:visibility="gone"> + android:orientation="vertical" + android:paddingBottom="@dimen/global_padding_bottom"> - + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + app:layout_constraintTop_toBottomOf="@+id/player_big_progress_bar" + app:layout_constraintEnd_toEndOf="parent" + android:orientation="horizontal"> + + + + + + app:layout_constraintTop_toBottomOf="@+id/head_title_favorite_linear_layout" /> - + app:layout_constraintTop_toBottomOf="@+id/player_artist_name_label"/> + \ No newline at end of file diff --git a/app/src/main/res/layout/player_header_bottom_sheet.xml b/app/src/main/res/layout/player_header_bottom_sheet.xml index 270f1be8..885250ee 100644 --- a/app/src/main/res/layout/player_header_bottom_sheet.xml +++ b/app/src/main/res/layout/player_header_bottom_sheet.xml @@ -19,26 +19,30 @@ + android:textStyle="bold" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/player_header_button" + app:layout_constraintTop_toTopOf="parent" />