재생목록 상세

- 하단에 미니 플레이어 추가
This commit is contained in:
klaus 2024-12-13 20:53:49 +09:00
parent 316c4399ce
commit c83a865032
5 changed files with 283 additions and 17 deletions

View File

@ -13,12 +13,14 @@ import android.view.ViewGroup
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.BundleCompat
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController import androidx.media3.session.MediaController
import androidx.media3.session.SessionCommand import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import androidx.media3.session.SessionToken import androidx.media3.session.SessionToken
import coil.load import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
@ -26,6 +28,7 @@ import coil.transform.RoundedCornersTransformation
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.orhanobut.logger.Logger
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
@ -95,9 +98,9 @@ class AudioContentPlayerFragment(
} }
override fun onDestroyView() { override fun onDestroyView() {
handler.removeCallbacksAndMessages(null)
mediaController?.release() mediaController?.release()
mediaController = null mediaController = null
handler.removeCallbacksAndMessages(null)
super.onDestroyView() super.onDestroyView()
} }
@ -170,13 +173,6 @@ class AudioContentPlayerFragment(
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
mediaController?.let { mediaController?.let {
when (playbackState) { when (playbackState) {
Player.STATE_READY -> {
binding.sbProgress.max = it.duration.toInt()
binding.tvTotalTime.text = Utils.convertDurationToString(
it.duration.toInt()
)
}
Player.STATE_ENDED -> it.seekTo(0) Player.STATE_ENDED -> it.seekTo(0)
else -> {} else -> {}
} }
@ -212,6 +208,26 @@ class AudioContentPlayerFragment(
} }
val sessionCommand = SessionCommand("UPDATE_PLAYLIST", Bundle.EMPTY) val sessionCommand = SessionCommand("UPDATE_PLAYLIST", Bundle.EMPTY)
mediaController!!.sendCustomCommand(sessionCommand, extras) mediaController!!.sendCustomCommand(sessionCommand, extras)
} else {
context?.let {
val sessionCommand = SessionCommand("GET_PLAYLIST", Bundle.EMPTY)
val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY)
resultFuture.addListener(
{
val result = resultFuture.get()
if (result.resultCode == SessionResult.RESULT_SUCCESS) {
val data = BundleCompat.getParcelableArrayList(
result.extras,
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
AudioContentPlaylistContent::class.java
)
Logger.e("playlist: $data")
}
},
ContextCompat.getMainExecutor(it)
)
}
} }
} }
@ -297,8 +313,13 @@ class AudioContentPlayerFragment(
private fun updateTimeUI() { private fun updateTimeUI() {
mediaController?.let { mediaController?.let {
val duration = it.duration
val currentPosition = it.currentPosition val currentPosition = it.currentPosition
binding.sbProgress.max = duration.toInt()
binding.sbProgress.progress = currentPosition.toInt() binding.sbProgress.progress = currentPosition.toInt()
binding.tvTotalTime.text = Utils.convertDurationToString(duration.toInt())
binding.tvProgressTime.text = Utils.convertDurationToString(currentPosition.toInt()) binding.tvProgressTime.text = Utils.convertDurationToString(currentPosition.toInt())
} }
} }

View File

@ -41,6 +41,8 @@ class AudioContentPlayerService : MediaSessionService() {
val compositeDisposable = CompositeDisposable() val compositeDisposable = CompositeDisposable()
private val playlist = ArrayList<AudioContentPlaylistContent>()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -81,6 +83,14 @@ class AudioContentPlayerService : MediaSessionService() {
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == "STOP_SERVICE") {
onStopService()
}
return super.onStartCommand(intent, flags, startId)
}
private fun initPlayer() { private fun initPlayer() {
player = ExoPlayer.Builder(this).build() player = ExoPlayer.Builder(this).build()
player!!.addListener(object : Player.Listener { player!!.addListener(object : Player.Listener {
@ -116,7 +126,7 @@ class AudioContentPlayerService : MediaSessionService() {
.add(SessionCommand("UPDATE_PLAYLIST", Bundle.EMPTY)) .add(SessionCommand("UPDATE_PLAYLIST", Bundle.EMPTY))
.add(SessionCommand("PLAY_NEXT_CONTENT", Bundle.EMPTY)) .add(SessionCommand("PLAY_NEXT_CONTENT", Bundle.EMPTY))
.add(SessionCommand("PLAY_PREVIOUS_CONTENT", Bundle.EMPTY)) .add(SessionCommand("PLAY_PREVIOUS_CONTENT", Bundle.EMPTY))
.add(SessionCommand("SEEK_TO", Bundle.EMPTY)) .add(SessionCommand("GET_PLAYLIST", Bundle.EMPTY))
.build() .build()
return MediaSession.ConnectionResult.AcceptedResultBuilder(session) return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
@ -139,6 +149,7 @@ class AudioContentPlayerService : MediaSessionService() {
) )
if (playlist != null) { if (playlist != null) {
this@AudioContentPlayerService.playlist.addAll(playlist)
playlistManager = AudioContentPlaylistManager(playlist) playlistManager = AudioContentPlaylistManager(playlist)
playNextContent() playNextContent()
} }
@ -156,6 +167,22 @@ class AudioContentPlayerService : MediaSessionService() {
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
} }
"GET_PLAYLIST" -> {
val extras = Bundle().apply {
putParcelableArrayList(
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
playlist
)
}
Futures.immediateFuture(
SessionResult(
SessionResult.RESULT_SUCCESS,
extras
)
)
}
else -> super.onCustomCommand(session, controller, customCommand, args) else -> super.onCustomCommand(session, controller, customCommand, args)
} }
} }
@ -170,6 +197,12 @@ class AudioContentPlayerService : MediaSessionService() {
stopForeground(true) stopForeground(true)
} }
mediaSession?.run {
player.release()
release()
mediaSession = null
}
stopSelf() stopSelf()
} }

View File

@ -1,26 +1,40 @@
package kr.co.vividnext.sodalive.audio_content.playlist.detail package kr.co.vividnext.sodalive.audio_content.playlist.detail
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
import kr.co.vividnext.sodalive.audio_content.playlist.modify.AudioContentPlaylistModifyActivity import kr.co.vividnext.sodalive.audio_content.playlist.modify.AudioContentPlaylistModifyActivity
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistDetailBinding import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistDetailBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import kotlin.random.Random
@UnstableApi @UnstableApi
class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlaylistDetailBinding>( class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlaylistDetailBinding>(
@ -34,10 +48,6 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
private var playlistId: Long = 0 private var playlistId: Long = 0
private val playerFragment: AudioContentPlayerFragment by lazy {
AudioContentPlayerFragment(screenWidth, ArrayList(contentList))
}
private val modifyPlaylistResult = registerForActivityResult( private val modifyPlaylistResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
) { result -> ) { result ->
@ -47,6 +57,101 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
} }
private val contentList = mutableListOf<AudioContentPlaylistContent>() private val contentList = mutableListOf<AudioContentPlaylistContent>()
private var mediaController: MediaController? = null
private val handler = Handler(Looper.getMainLooper())
@SuppressLint("SetTextI18n")
private val preferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
// 특정 키에 대한 값이 변경될 때 UI 업데이트
if (key == Constants.PREF_IS_PLAYER_SERVICE_RUNNING) {
handler.postDelayed(
{
if (sharedPreferences.getBoolean(key, false)) {
initAndVisibleMiniPlayer()
} else {
deInitMiniPlayer()
}
},
500
)
}
}
private fun initAndVisibleMiniPlayer() {
binding.clMiniPlayer.visibility = View.VISIBLE
binding.clMiniPlayer.setOnClickListener { showPlayerFragment() }
binding.ivStop.setOnClickListener {
val stopIntent = Intent(
applicationContext,
AudioContentPlayerService::class.java
)
stopIntent.action = "STOP_SERVICE"
startService(stopIntent)
}
connectPlayerService()
}
private fun connectPlayerService() {
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
val sessionToken = SessionToken(applicationContext, componentName)
val mediaControllerFuture =
MediaController.Builder(applicationContext, sessionToken).buildAsync()
mediaControllerFuture.addListener(
{
mediaController = mediaControllerFuture.get()
setupMediaController()
binding.ivPlayOrPause.setOnClickListener {
mediaController?.let {
if (it.playWhenReady) {
it.pause()
} else {
it.play()
}
}
}
},
ContextCompat.getMainExecutor(applicationContext)
)
}
private fun setupMediaController() {
if (mediaController == null) {
deInitMiniPlayer()
return
}
mediaController!!.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
mediaItem?.mediaMetadata?.let {
binding.ivPlayerCover.load(it.artworkUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvPlayerTitle.text = it.title
binding.tvPlayerNickname.text = it.artist
}
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
binding.ivPlayOrPause.setImageResource(
if (playWhenReady) {
R.drawable.ic_player_pause
} else {
R.drawable.ic_player_play
}
)
}
})
}
private fun deInitMiniPlayer() {
binding.clMiniPlayer.visibility = View.GONE
mediaController?.release()
mediaController = null
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
playlistId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID, 0) playlistId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID, 0)
@ -59,6 +164,26 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
bindData() bindData()
viewModel.getPlaylistDetail(playlistId) viewModel.getPlaylistDetail(playlistId)
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
if (SharedPreferenceManager.isPlayerServiceRunning) {
initAndVisibleMiniPlayer()
} else {
deInitMiniPlayer()
}
}
override fun onDestroy() {
deInitMiniPlayer()
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
super.onDestroy()
}
private fun showPlayerFragment(
contentList: ArrayList<AudioContentPlaylistContent> = arrayListOf()
) {
val playerFragment = AudioContentPlayerFragment(screenWidth, contentList)
playerFragment.show(supportFragmentManager, playerFragment.tag)
} }
override fun setupView() { override fun setupView() {
@ -105,10 +230,13 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
recyclerView.adapter = adapter recyclerView.adapter = adapter
binding.llPlay.setOnClickListener { binding.llPlay.setOnClickListener {
if (playerFragment.isAdded) return@setOnClickListener showPlayerFragment(contentList = ArrayList(contentList))
playerFragment.show(supportFragmentManager, playerFragment.tag)
} }
binding.llShuffle.setOnClickListener { } binding.llShuffle.setOnClickListener {
val shuffledList = ArrayList(contentList).apply { shuffle(Random) }
showPlayerFragment(contentList = shuffledList)
}
binding.tvBack.setOnClickListener { finish() } binding.tvBack.setOnClickListener { finish() }
binding.ivEdit.setOnClickListener { binding.ivEdit.setOnClickListener {
modifyPlaylistResult.launch( modifyPlaylistResult.launch(

View File

@ -14,6 +14,18 @@ object SharedPreferenceManager {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
} }
fun registerOnSharedPreferenceChangeListener(
listener: SharedPreferences.OnSharedPreferenceChangeListener
) {
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
}
fun unregisterOnSharedPreferenceChangeListener(
listener: SharedPreferences.OnSharedPreferenceChangeListener
) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
}
fun clear() { fun clear() {
sharedPreferences.edit { it.clear() } sharedPreferences.edit { it.clear() }
} }

View File

@ -286,9 +286,81 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginVertical="18dp" android:layout_marginVertical="18dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/cl_mini_player"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_play" /> app:layout_constraintTop_toBottomOf="@+id/ll_play" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_mini_player"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/color_222222"
android:paddingHorizontal="13.3dp"
android:paddingVertical="10.7dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<ImageView
android:id="@+id/iv_player_cover"
android:layout_width="36.7dp"
android:layout_height="36.7dp"
android:layout_centerVertical="true"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tv_player_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10.7dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/color_eeeeee"
android:textSize="13sp"
app:layout_constraintEnd_toStartOf="@+id/iv_play_or_pause"
app:layout_constraintStart_toEndOf="@+id/iv_player_cover"
app:layout_constraintTop_toTopOf="@+id/iv_player_cover"
tools:text="JFLA 커버곡 Avicii for your self" />
<TextView
android:id="@+id/tv_player_nickname"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2.3dp"
android:textColor="@color/color_d2d2d2"
android:textSize="11sp"
app:layout_constraintEnd_toEndOf="@+id/tv_player_title"
app:layout_constraintStart_toStartOf="@+id/tv_player_title"
app:layout_constraintTop_toBottomOf="@+id/tv_player_title"
tools:ignore="SmallSp"
tools:text="JFLA 커버곡 Avicii for your self" />
<ImageView
android:id="@+id/iv_play_or_pause"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="16dp"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="@+id/iv_stop"
app:layout_constraintEnd_toStartOf="@+id/iv_stop"
app:layout_constraintTop_toTopOf="@+id/iv_stop"
tools:src="@drawable/btn_bar_play" />
<ImageView
android:id="@+id/iv_stop"
android:layout_width="25dp"
android:layout_height="25dp"
android:contentDescription="@null"
android:src="@drawable/ic_noti_stop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout> </LinearLayout>