parent
40e82a3796
commit
316c4399ce
|
@ -162,8 +162,6 @@ dependencies {
|
|||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
|
||||
|
||||
implementation "androidx.media3:media3-ui:1.4.1"
|
||||
implementation "androidx.media3:media3-session:1.4.1"
|
||||
implementation "androidx.media3:media3-exoplayer:1.4.1"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:1.4.1"
|
||||
}
|
||||
|
|
|
@ -182,6 +182,15 @@
|
|||
android:foregroundServiceType="mediaPlayback"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<service
|
||||
android:name=".audio_content.player.AudioContentPlayerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- [START firebase_service] -->
|
||||
<service
|
||||
android:name=".fcm.SodaFirebaseMessagingService"
|
||||
|
|
|
@ -1,22 +1,44 @@
|
|||
package kr.co.vividnext.sodalive.audio_content.player
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionToken
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.Utils
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentPlayerBinding
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
@UnstableApi
|
||||
class AudioContentPlayerFragment(
|
||||
private val screenWidth: Int,
|
||||
private val playlist: List<AudioContentPlaylistContent>
|
||||
private val playlist: ArrayList<AudioContentPlaylistContent>
|
||||
) : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
@ -24,6 +46,10 @@ class AudioContentPlayerFragment(
|
|||
|
||||
private val viewModel: AudioContentPlayerViewModel by viewModel()
|
||||
|
||||
private var mediaController: MediaController? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var isUserSeeking = false
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
|
||||
|
||||
|
@ -65,6 +91,14 @@ class AudioContentPlayerFragment(
|
|||
|
||||
setupView()
|
||||
bindData()
|
||||
connectPlayerService()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
mediaController?.release()
|
||||
mediaController = null
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
|
@ -88,4 +122,196 @@ class AudioContentPlayerFragment(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectPlayerService() {
|
||||
context?.let {
|
||||
if (!SharedPreferenceManager.isPlayerServiceRunning) {
|
||||
startPlayerService(context = it)
|
||||
}
|
||||
|
||||
view?.postDelayed({
|
||||
connectToMediaSession(it)
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPlayerService(context: Context) {
|
||||
val serviceIntent = Intent(context, AudioContentPlayerService::class.java)
|
||||
context.startService(serviceIntent)
|
||||
}
|
||||
|
||||
private fun connectToMediaSession(context: Context) {
|
||||
val componentName = ComponentName(context, AudioContentPlayerService::class.java)
|
||||
val sessionToken = SessionToken(context, componentName)
|
||||
val mediaControllerFuture = MediaController.Builder(context, sessionToken).buildAsync()
|
||||
mediaControllerFuture.addListener(
|
||||
{
|
||||
mediaController = mediaControllerFuture.get()
|
||||
setupMediaController()
|
||||
updatePlayerUI()
|
||||
startUpdatingUI()
|
||||
},
|
||||
ContextCompat.getMainExecutor(context)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupMediaController() {
|
||||
if (mediaController == null) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
"플레이어를 실행하지 못했습니다.\n다시 시도해 주세요.",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
mediaController!!.addListener(object : Player.Listener {
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
mediaController?.let {
|
||||
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)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
updateMediaMetadata(mediaItem?.mediaMetadata)
|
||||
updateTimeUI()
|
||||
}
|
||||
|
||||
override fun onIsLoadingChanged(isLoading: Boolean) {
|
||||
viewModel.setLoading(isLoading)
|
||||
}
|
||||
|
||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
if (playWhenReady) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
R.drawable.ic_player_play
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (playlist.isNotEmpty()) {
|
||||
val extras = Bundle().apply {
|
||||
putParcelableArrayList(
|
||||
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
|
||||
playlist
|
||||
)
|
||||
}
|
||||
val sessionCommand = SessionCommand("UPDATE_PLAYLIST", Bundle.EMPTY)
|
||||
mediaController!!.sendCustomCommand(sessionCommand, extras)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlayerUI() {
|
||||
binding.ivPlayOrPause.setOnClickListener {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
} else {
|
||||
it.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.ivSkipForward.setOnClickListener {
|
||||
mediaController?.let {
|
||||
val sessionCommand = SessionCommand(
|
||||
"PLAY_NEXT_CONTENT",
|
||||
Bundle.EMPTY
|
||||
)
|
||||
it.sendCustomCommand(sessionCommand, Bundle.EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
binding.ivSkipBack.setOnClickListener {
|
||||
mediaController?.let {
|
||||
val sessionCommand = SessionCommand(
|
||||
"PLAY_PREVIOUS_CONTENT",
|
||||
Bundle.EMPTY
|
||||
)
|
||||
it.sendCustomCommand(sessionCommand, Bundle.EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
binding.sbProgress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar?,
|
||||
progress: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
if (fromUser) {
|
||||
isUserSeeking = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(p0: SeekBar?) {
|
||||
isUserSeeking = true
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
isUserSeeking = false
|
||||
seekBar?.progress?.let { progress ->
|
||||
mediaController?.seekTo(progress.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
updateMediaMetadata(mediaController?.currentMediaItem?.mediaMetadata)
|
||||
updateTimeUI()
|
||||
}
|
||||
|
||||
private fun updateMediaMetadata(metadata: MediaMetadata?) {
|
||||
metadata?.let {
|
||||
binding.tvTitle.text = it.title
|
||||
binding.tvCreatorNickname.text = it.artist
|
||||
|
||||
binding.ivCreatorProfile.load(
|
||||
it.extras?.getString(Constants.EXTRA_AUDIO_CONTENT_CREATOR_PROFILE_IMAGE)
|
||||
) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
|
||||
binding.ivCover.load(it.artworkUri) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(RoundedCornersTransformation(4f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTimeUI() {
|
||||
mediaController?.let {
|
||||
val currentPosition = it.currentPosition
|
||||
binding.sbProgress.progress = currentPosition.toInt()
|
||||
binding.tvProgressTime.text = Utils.convertDurationToString(currentPosition.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
private fun startUpdatingUI() {
|
||||
handler.post(object : Runnable {
|
||||
override fun run() {
|
||||
if (mediaController?.isPlaying == true && !isUserSeeking) {
|
||||
updateTimeUI()
|
||||
}
|
||||
|
||||
handler.postDelayed(this, 500)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
package kr.co.vividnext.sodalive.audio_content.player
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.extractor.DefaultExtractorsFactory
|
||||
import androidx.media3.extractor.ts.AdtsExtractor
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionResult
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.Utils
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@UnstableApi
|
||||
class AudioContentPlayerService : MediaSessionService() {
|
||||
|
||||
private val repository: AudioContentGenerateUrlRepository by inject()
|
||||
|
||||
private var playlistManager: AudioContentPlaylistManager? = null
|
||||
private var mediaSession: MediaSession? = null
|
||||
private var player: ExoPlayer? = null
|
||||
|
||||
val compositeDisposable = CompositeDisposable()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
try {
|
||||
SharedPreferenceManager.isPlayerServiceRunning = true
|
||||
|
||||
initPlayer()
|
||||
initMediaSession()
|
||||
} catch (e: Exception) {
|
||||
onStopService()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaSession?.run {
|
||||
player.release()
|
||||
release()
|
||||
mediaSession = null
|
||||
}
|
||||
|
||||
compositeDisposable.dispose()
|
||||
SharedPreferenceManager.isPlayerServiceRunning = false
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
mediaSession?.run {
|
||||
if (
|
||||
!player.playWhenReady ||
|
||||
player.mediaItemCount == 0 ||
|
||||
player.playbackState == Player.STATE_ENDED
|
||||
) {
|
||||
onStopService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession
|
||||
|
||||
private fun initPlayer() {
|
||||
player = ExoPlayer.Builder(this).build()
|
||||
player!!.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
super.onMediaItemTransition(mediaItem, reason)
|
||||
player?.play()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
if (playbackState == Player.STATE_IDLE) {
|
||||
onStopService()
|
||||
} else if (playbackState == Player.STATE_ENDED) {
|
||||
if (playlistManager!!.hasNextContent()) {
|
||||
playNextContent()
|
||||
} else {
|
||||
onStopService()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initMediaSession() {
|
||||
mediaSession = MediaSession.Builder(this, player!!)
|
||||
.setCallback(object : MediaSession.Callback {
|
||||
override fun onConnect(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
val allowedCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS
|
||||
.buildUpon()
|
||||
.add(SessionCommand("UPDATE_PLAYLIST", Bundle.EMPTY))
|
||||
.add(SessionCommand("PLAY_NEXT_CONTENT", Bundle.EMPTY))
|
||||
.add(SessionCommand("PLAY_PREVIOUS_CONTENT", Bundle.EMPTY))
|
||||
.add(SessionCommand("SEEK_TO", Bundle.EMPTY))
|
||||
.build()
|
||||
|
||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(allowedCommands)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
return when (customCommand.customAction) {
|
||||
"UPDATE_PLAYLIST" -> {
|
||||
val playlist = BundleCompat.getParcelableArrayList(
|
||||
args,
|
||||
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
|
||||
AudioContentPlaylistContent::class.java
|
||||
)
|
||||
|
||||
if (playlist != null) {
|
||||
playlistManager = AudioContentPlaylistManager(playlist)
|
||||
playNextContent()
|
||||
}
|
||||
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
"PLAY_NEXT_CONTENT" -> {
|
||||
playNextContent()
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
"PLAY_PREVIOUS_CONTENT" -> {
|
||||
playPreviousContent()
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
else -> super.onCustomCommand(session, controller, customCommand, args)
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun onStopService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun playNextContent() {
|
||||
val content = playlistManager?.moveToNext()
|
||||
|
||||
if (content != null) {
|
||||
generateUrl(
|
||||
content.id,
|
||||
onSuccess = { urlGenerateSuccess(content, it) },
|
||||
onFailure = {
|
||||
if (playlistManager!!.hasNextContent()) {
|
||||
playNextContent()
|
||||
} else {
|
||||
onStopService()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun urlGenerateSuccess(
|
||||
content: AudioContentPlaylistContent,
|
||||
contentUrl: String
|
||||
) {
|
||||
val extras = Bundle().apply {
|
||||
putString(
|
||||
Constants.EXTRA_AUDIO_CONTENT_CREATOR_PROFILE_IMAGE,
|
||||
content.creatorProfileUrl
|
||||
)
|
||||
}
|
||||
|
||||
val mediaMetadata = MediaMetadata.Builder()
|
||||
.setTitle(content.title)
|
||||
.setArtist(content.creatorNickname)
|
||||
.setArtworkUri(Uri.parse(content.coverUrl))
|
||||
.setExtras(extras)
|
||||
.setDurationMs(Utils.convertStringToDuration(content.duration))
|
||||
.build()
|
||||
|
||||
val mediaItem = MediaItem.Builder()
|
||||
.setUri(Uri.parse(contentUrl))
|
||||
.setMediaMetadata(mediaMetadata)
|
||||
.build()
|
||||
|
||||
val extractorFactory = DefaultExtractorsFactory().setAdtsExtractorFlags(
|
||||
AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
|
||||
)
|
||||
val mediaSource = ProgressiveMediaSource.Factory(
|
||||
DefaultDataSource.Factory(this),
|
||||
extractorFactory
|
||||
).createMediaSource(mediaItem)
|
||||
|
||||
player?.setMediaSource(mediaSource)
|
||||
player?.prepare()
|
||||
}
|
||||
|
||||
private fun playPreviousContent() {
|
||||
val content = playlistManager?.moveToPrevious()
|
||||
|
||||
if (content != null) {
|
||||
generateUrl(
|
||||
content.id,
|
||||
onSuccess = { urlGenerateSuccess(content, it) },
|
||||
onFailure = {
|
||||
if (playlistManager!!.hasNextContent()) {
|
||||
playNextContent()
|
||||
} else {
|
||||
onStopService()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateUrl(contentId: Long, onSuccess: (String) -> Unit, onFailure: () -> Unit) {
|
||||
if (contentId <= 0) {
|
||||
onFailure()
|
||||
}
|
||||
|
||||
compositeDisposable.add(
|
||||
repository.generateUrl(
|
||||
contentId = contentId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
onSuccess(it.data.contentUrl)
|
||||
} else {
|
||||
onFailure()
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
onFailure()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -19,43 +19,7 @@ class AudioContentPlayerViewModel(
|
|||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
fun generateUrl(contentId: Long, onSuccess: (String) -> Unit, onFailure: () -> Unit) {
|
||||
if (contentId <= 0) {
|
||||
onFailure()
|
||||
}
|
||||
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.generateUrl(
|
||||
contentId = contentId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
onSuccess(it.data.contentUrl)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
}
|
||||
|
||||
onFailure()
|
||||
}
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
onFailure()
|
||||
}
|
||||
)
|
||||
)
|
||||
fun setLoading(loading: Boolean) {
|
||||
_isLoading.value = loading
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package kr.co.vividnext.sodalive.audio_content.player
|
||||
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
|
||||
class AudioContentPlaylistManager(private val playlist: List<AudioContentPlaylistContent>) {
|
||||
private var currentIndex = -1
|
||||
|
||||
fun moveToNext(): AudioContentPlaylistContent? {
|
||||
if (playlist.isNotEmpty()) {
|
||||
currentIndex = if (currentIndex + 1 >= playlist.size) 0 else currentIndex + 1
|
||||
return playlist[currentIndex]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun moveToPrevious(): AudioContentPlaylistContent? {
|
||||
if (playlist.isNotEmpty()) {
|
||||
currentIndex = if (currentIndex - 1 < 0) playlist.size - 1 else currentIndex - 1
|
||||
return playlist[currentIndex]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun hasNextContent(): Boolean {
|
||||
return currentIndex + 1 < playlist.size
|
||||
}
|
||||
}
|
|
@ -40,7 +40,8 @@ class AudioContentPlaylistCreateActivity : BaseActivity<ActivityAudioContentPlay
|
|||
category = item.themeStr,
|
||||
coverUrl = item.coverImageUrl,
|
||||
duration = item.duration ?: "00:00:00",
|
||||
creatorNickname = item.creatorNickname
|
||||
creatorNickname = item.creatorNickname,
|
||||
creatorProfileUrl = ""
|
||||
)
|
||||
)
|
||||
return@PlaylistAddContentDialogFragment true
|
||||
|
@ -54,7 +55,8 @@ class AudioContentPlaylistCreateActivity : BaseActivity<ActivityAudioContentPlay
|
|||
category = item.themeStr,
|
||||
coverUrl = item.coverImageUrl,
|
||||
duration = item.duration ?: "00:00:00",
|
||||
creatorNickname = item.creatorNickname
|
||||
creatorNickname = item.creatorNickname,
|
||||
creatorProfileUrl = ""
|
||||
)
|
||||
)
|
||||
return@PlaylistAddContentDialogFragment true
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.view.View
|
|||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
|
@ -21,6 +22,7 @@ import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistDetailBi
|
|||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@UnstableApi
|
||||
class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlaylistDetailBinding>(
|
||||
ActivityAudioContentPlaylistDetailBinding::inflate
|
||||
) {
|
||||
|
@ -33,7 +35,7 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
|||
private var playlistId: Long = 0
|
||||
|
||||
private val playerFragment: AudioContentPlayerFragment by lazy {
|
||||
AudioContentPlayerFragment(screenWidth, contentList)
|
||||
AudioContentPlayerFragment(screenWidth, ArrayList(contentList))
|
||||
}
|
||||
|
||||
private val modifyPlaylistResult = registerForActivityResult(
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package kr.co.vividnext.sodalive.audio_content.playlist.detail
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
data class GetPlaylistDetailResponse(
|
||||
|
@ -15,11 +17,13 @@ data class GetPlaylistDetailResponse(
|
|||
)
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
data class AudioContentPlaylistContent(
|
||||
@SerializedName("id") val id: Long,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("category") val category: String,
|
||||
@SerializedName("coverUrl") val coverUrl: String,
|
||||
@SerializedName("duration") val duration: String,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String
|
||||
)
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("creatorProfileUrl") val creatorProfileUrl: String
|
||||
) : Parcelable
|
||||
|
|
|
@ -46,7 +46,8 @@ class AudioContentPlaylistModifyActivity : BaseActivity<ActivityAudioContentPlay
|
|||
category = item.themeStr,
|
||||
coverUrl = item.coverImageUrl,
|
||||
duration = item.duration ?: "00:00:00",
|
||||
creatorNickname = item.creatorNickname
|
||||
creatorNickname = item.creatorNickname,
|
||||
creatorProfileUrl = ""
|
||||
)
|
||||
)
|
||||
return@PlaylistAddContentDialogFragment true
|
||||
|
@ -60,7 +61,8 @@ class AudioContentPlaylistModifyActivity : BaseActivity<ActivityAudioContentPlay
|
|||
category = item.themeStr,
|
||||
coverUrl = item.coverImageUrl,
|
||||
duration = item.duration ?: "00:00:00",
|
||||
creatorNickname = item.creatorNickname
|
||||
creatorNickname = item.creatorNickname,
|
||||
creatorProfileUrl = ""
|
||||
)
|
||||
)
|
||||
return@PlaylistAddContentDialogFragment true
|
||||
|
|
|
@ -15,6 +15,7 @@ object Constants {
|
|||
const val PREF_IS_CONTENT_PLAY_LOOP = "pref_is_content_play_loop"
|
||||
const val PREF_IS_ADULT_CONTENT_VISIBLE = "pref_is_adult_content_visible"
|
||||
const val PREF_IS_FOLLOWED_CREATOR_LIVE = "pref_is_followed_creator_live"
|
||||
const val PREF_IS_PLAYER_SERVICE_RUNNING = "pref_is_player_service_running"
|
||||
const val PREF_NOT_SHOWING_EVENT_POPUP_ID = "pref_not_showing_event_popup_id"
|
||||
const val PREF_IS_VIEWED_ON_BOARDING_TUTORIAL = "pref_is_viewed_on_boarding_tutorial"
|
||||
|
||||
|
@ -57,11 +58,13 @@ object Constants {
|
|||
const val EXTRA_AUDIO_CONTENT_LOADING = "audio_content_loading"
|
||||
const val EXTRA_AUDIO_CONTENT_CREATOR_ID = "audio_content_creator_id"
|
||||
const val EXTRA_AUDIO_CONTENT_CREATOR_NICKNAME = "audio_content_creator_nickname"
|
||||
const val EXTRA_AUDIO_CONTENT_CREATOR_PROFILE_IMAGE = "audio_content_creator_profile_image"
|
||||
const val EXTRA_AUDIO_CONTENT_CURATION_ID = "extra_audio_content_curation_id"
|
||||
const val EXTRA_AUDIO_CONTENT_CURATION_TITLE = "extra_audio_content_curation_title"
|
||||
const val EXTRA_AUDIO_CONTENT_NEXT_ACTION = "audio_content_next_action"
|
||||
const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview"
|
||||
const val EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL = "audio_content_cover_image_url"
|
||||
const val EXTRA_AUDIO_CONTENT_PLAYLIST = "extra_audio_content_playlist"
|
||||
const val EXTRA_IS_SHOW_SECRET = "extra_is_show_secret"
|
||||
|
||||
const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2
|
||||
|
|
|
@ -150,4 +150,10 @@ object SharedPreferenceManager {
|
|||
val listJson = gson.toJson(value)
|
||||
sharedPreferences[Constants.PREF_NO_CHAT_ROOM] = listJson
|
||||
}
|
||||
|
||||
var isPlayerServiceRunning: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_PLAYER_SERVICE_RUNNING, false]
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_PLAYER_SERVICE_RUNNING] = value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,4 +9,17 @@ object Utils {
|
|||
|
||||
return "%02d:%02d:%02d".format(hours, minutes, seconds)
|
||||
}
|
||||
|
||||
fun convertStringToDuration(timeString: String): Long {
|
||||
val parts = timeString.split(":")
|
||||
if (parts.size != 3) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val hours = parts[0].toLong()
|
||||
val minutes = parts[1].toLong()
|
||||
val seconds = parts[2].toLong()
|
||||
|
||||
return 1000 * (hours * 3600 + minutes * 60 + seconds)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue