diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerFragment.kt index b021d01..db2df66 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerFragment.kt @@ -117,6 +117,7 @@ class AudioContentPlayerFragment( } adapter = AudioContentPlaylistDetailAdapter { contentId -> + binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) val extras = Bundle().apply { putLong( Constants.EXTRA_AUDIO_CONTENT_ID, @@ -165,6 +166,24 @@ class AudioContentPlayerFragment( } }) recyclerView.adapter = adapter + + binding.ivLoopSegment.setOnClickListener { + val sessionCommand = SessionCommand("TOGGLE_SEGMENT_LOOP", Bundle.EMPTY) + val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY) + resultFuture.addListener( + { + val result = resultFuture.get() + if (result.resultCode == SessionResult.RESULT_SUCCESS) { + val imageRes = result.extras.getInt( + Constants.EXTRA_PLAYLIST_SEGMENT_LOOP_IMAGE, + R.drawable.ic_loop_segment_idle + ) + binding.ivLoopSegment.setImageResource(imageRes) + } + }, + ContextCompat.getMainExecutor(requireContext()) + ) + } } private fun bindData() { @@ -282,6 +301,7 @@ class AudioContentPlayerFragment( }) if (playlist.isNotEmpty()) { + binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) val extras = Bundle().apply { putParcelableArrayList( Constants.EXTRA_AUDIO_CONTENT_PLAYLIST, @@ -292,6 +312,7 @@ class AudioContentPlayerFragment( mediaController!!.sendCustomCommand(sessionCommand, extras) adapter.updateItems(playlist) } else { + binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) context?.let { val sessionCommand = SessionCommand("GET_PLAYLIST", Bundle.EMPTY) val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY) @@ -341,6 +362,7 @@ class AudioContentPlayerFragment( binding.ivSkipForward.setOnClickListener { mediaController?.let { + binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) val sessionCommand = SessionCommand( "PLAY_NEXT_CONTENT", Bundle.EMPTY @@ -351,6 +373,7 @@ class AudioContentPlayerFragment( binding.ivSkipBack.setOnClickListener { mediaController?.let { + binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) val sessionCommand = SessionCommand( "PLAY_PREVIOUS_CONTENT", Bundle.EMPTY @@ -361,6 +384,7 @@ class AudioContentPlayerFragment( binding.ivSeekForward10.setOnClickListener { mediaController?.let { + binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) val sessionCommand = SessionCommand( "SEEK_FORWARD", Bundle.EMPTY @@ -371,6 +395,7 @@ class AudioContentPlayerFragment( binding.ivSeekBackward10.setOnClickListener { mediaController?.let { + binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) val sessionCommand = SessionCommand( "SEEK_BACKWARD", Bundle.EMPTY diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerService.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerService.kt index bf5b259..56a8e88 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerService.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerService.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import androidx.core.os.BundleCompat import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata @@ -25,6 +27,7 @@ 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.R import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.SharedPreferenceManager @@ -45,6 +48,12 @@ class AudioContentPlayerService : MediaSessionService() { private val playlist = ArrayList() + private var loopStartMs: Long? = null + private var loopEndMs: Long? = null + private var isLooping = false + private val loopHandler = Handler(Looper.getMainLooper()) + private val loopCheckInterval = 100L // 0.1초 간격 + override fun onCreate() { super.onCreate() @@ -66,6 +75,7 @@ class AudioContentPlayerService : MediaSessionService() { compositeDisposable.dispose() SharedPreferenceManager.isPlayerServiceRunning = false + stopLoop() super.onDestroy() } @@ -83,6 +93,44 @@ class AudioContentPlayerService : MediaSessionService() { return super.onStartCommand(intent, flags, startId) } + private fun toggleSegmentLoop() { + when { + loopStartMs == null -> { + loopStartMs = player?.currentPosition + } + + loopEndMs == null -> { + loopEndMs = player?.currentPosition + isLooping = true + startLoopMonitoring() + } + + else -> { + stopLoop() + } + } + } + + private fun startLoopMonitoring() { + loopHandler.post(object : Runnable { + override fun run() { + if (isLooping && loopStartMs != null && loopEndMs != null && player != null) { + if (player!!.currentPosition >= loopEndMs!!) { + player!!.seekTo(loopStartMs!!) + } + loopHandler.postDelayed(this, loopCheckInterval) + } + } + }) + } + + private fun stopLoop() { + isLooping = false + loopStartMs = null + loopEndMs = null + loopHandler.removeCallbacksAndMessages(null) + } + private fun initPlayer() { player = ExoPlayer.Builder(this).build() player!!.addListener(object : Player.Listener { @@ -132,6 +180,7 @@ class AudioContentPlayerService : MediaSessionService() { .add(SessionCommand("GET_PLAYLIST", Bundle.EMPTY)) .add(SessionCommand("SEEK_FORWARD", Bundle.EMPTY)) .add(SessionCommand("SEEK_BACKWARD", Bundle.EMPTY)) + .add(SessionCommand("TOGGLE_SEGMENT_LOOP", Bundle.EMPTY)) .build() return MediaSession.ConnectionResult.AcceptedResultBuilder(session) @@ -147,6 +196,7 @@ class AudioContentPlayerService : MediaSessionService() { ): ListenableFuture { return when (customCommand.customAction) { "UPDATE_PLAYLIST" -> { + stopLoop() val playlist = BundleCompat.getParcelableArrayList( args, Constants.EXTRA_AUDIO_CONTENT_PLAYLIST, @@ -163,31 +213,66 @@ class AudioContentPlayerService : MediaSessionService() { } "PLAY_NEXT_CONTENT" -> { + stopLoop() playNextContent() Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } "PLAY_PREVIOUS_CONTENT" -> { + stopLoop() playPreviousContent() Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } "PLAY_SELECTED_CONTENT" -> { + stopLoop() val selectedContentId = args.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) playSelectedContent(contentId = selectedContentId) Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } "SEEK_FORWARD" -> { + stopLoop() playSeekForward() Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } "SEEK_BACKWARD" -> { + stopLoop() playSeekBackward() Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } + "TOGGLE_SEGMENT_LOOP" -> { + val extras = Bundle().apply { + putInt( + Constants.EXTRA_PLAYLIST_SEGMENT_LOOP_IMAGE, + when { + loopStartMs == null -> { + R.drawable.ic_loop_segment_start_set + } + + loopEndMs == null -> { + R.drawable.ic_loop_segment_active + } + + else -> { + R.drawable.ic_loop_segment_idle + } + } + ) + } + + toggleSegmentLoop() + + Futures.immediateFuture( + SessionResult( + SessionResult.RESULT_SUCCESS, + extras + ) + ) + } + "GET_PLAYLIST" -> { val extras = Bundle().apply { putParcelableArrayList( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt index d2e7d13..eef664c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt @@ -77,6 +77,7 @@ object Constants { 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_PLAYLIST_SEGMENT_LOOP_IMAGE = "extra_playlist_segment_loop_image" const val EXTRA_IS_SHOW_SECRET = "extra_is_show_secret" const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2 diff --git a/app/src/main/res/drawable-xxhdpi/ic_loop_segment_active.png b/app/src/main/res/drawable-xxhdpi/ic_loop_segment_active.png new file mode 100755 index 0000000..ac9aec9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_loop_segment_active.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_loop_segment_idle.png b/app/src/main/res/drawable-xxhdpi/ic_loop_segment_idle.png new file mode 100755 index 0000000..e88a42a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_loop_segment_idle.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_loop_segment_start_set.png b/app/src/main/res/drawable-xxhdpi/ic_loop_segment_start_set.png new file mode 100755 index 0000000..2703ce5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_loop_segment_start_set.png differ diff --git a/app/src/main/res/layout/fragment_audio_content_player.xml b/app/src/main/res/layout/fragment_audio_content_player.xml index 847045c..e9e53ae 100644 --- a/app/src/main/res/layout/fragment_audio_content_player.xml +++ b/app/src/main/res/layout/fragment_audio_content_player.xml @@ -165,6 +165,16 @@ app:layout_constraintStart_toEndOf="@+id/iv_play_or_pause" app:layout_constraintTop_toTopOf="@+id/iv_play_or_pause" /> + +