구간반복 기능 추가
This commit is contained in:
		| @@ -117,6 +117,7 @@ class AudioContentPlayerFragment( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         adapter = AudioContentPlaylistDetailAdapter { contentId -> |         adapter = AudioContentPlaylistDetailAdapter { contentId -> | ||||||
|  |             binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) | ||||||
|             val extras = Bundle().apply { |             val extras = Bundle().apply { | ||||||
|                 putLong( |                 putLong( | ||||||
|                     Constants.EXTRA_AUDIO_CONTENT_ID, |                     Constants.EXTRA_AUDIO_CONTENT_ID, | ||||||
| @@ -165,6 +166,24 @@ class AudioContentPlayerFragment( | |||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|         recyclerView.adapter = adapter |         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() { |     private fun bindData() { | ||||||
| @@ -282,6 +301,7 @@ class AudioContentPlayerFragment( | |||||||
|         }) |         }) | ||||||
|  |  | ||||||
|         if (playlist.isNotEmpty()) { |         if (playlist.isNotEmpty()) { | ||||||
|  |             binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) | ||||||
|             val extras = Bundle().apply { |             val extras = Bundle().apply { | ||||||
|                 putParcelableArrayList( |                 putParcelableArrayList( | ||||||
|                     Constants.EXTRA_AUDIO_CONTENT_PLAYLIST, |                     Constants.EXTRA_AUDIO_CONTENT_PLAYLIST, | ||||||
| @@ -292,6 +312,7 @@ class AudioContentPlayerFragment( | |||||||
|             mediaController!!.sendCustomCommand(sessionCommand, extras) |             mediaController!!.sendCustomCommand(sessionCommand, extras) | ||||||
|             adapter.updateItems(playlist) |             adapter.updateItems(playlist) | ||||||
|         } else { |         } else { | ||||||
|  |             binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) | ||||||
|             context?.let { |             context?.let { | ||||||
|                 val sessionCommand = SessionCommand("GET_PLAYLIST", Bundle.EMPTY) |                 val sessionCommand = SessionCommand("GET_PLAYLIST", Bundle.EMPTY) | ||||||
|                 val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY) |                 val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY) | ||||||
| @@ -341,6 +362,7 @@ class AudioContentPlayerFragment( | |||||||
|  |  | ||||||
|         binding.ivSkipForward.setOnClickListener { |         binding.ivSkipForward.setOnClickListener { | ||||||
|             mediaController?.let { |             mediaController?.let { | ||||||
|  |                 binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) | ||||||
|                 val sessionCommand = SessionCommand( |                 val sessionCommand = SessionCommand( | ||||||
|                     "PLAY_NEXT_CONTENT", |                     "PLAY_NEXT_CONTENT", | ||||||
|                     Bundle.EMPTY |                     Bundle.EMPTY | ||||||
| @@ -351,6 +373,7 @@ class AudioContentPlayerFragment( | |||||||
|  |  | ||||||
|         binding.ivSkipBack.setOnClickListener { |         binding.ivSkipBack.setOnClickListener { | ||||||
|             mediaController?.let { |             mediaController?.let { | ||||||
|  |                 binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) | ||||||
|                 val sessionCommand = SessionCommand( |                 val sessionCommand = SessionCommand( | ||||||
|                     "PLAY_PREVIOUS_CONTENT", |                     "PLAY_PREVIOUS_CONTENT", | ||||||
|                     Bundle.EMPTY |                     Bundle.EMPTY | ||||||
| @@ -361,6 +384,7 @@ class AudioContentPlayerFragment( | |||||||
|  |  | ||||||
|         binding.ivSeekForward10.setOnClickListener { |         binding.ivSeekForward10.setOnClickListener { | ||||||
|             mediaController?.let { |             mediaController?.let { | ||||||
|  |                 binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) | ||||||
|                 val sessionCommand = SessionCommand( |                 val sessionCommand = SessionCommand( | ||||||
|                     "SEEK_FORWARD", |                     "SEEK_FORWARD", | ||||||
|                     Bundle.EMPTY |                     Bundle.EMPTY | ||||||
| @@ -371,6 +395,7 @@ class AudioContentPlayerFragment( | |||||||
|  |  | ||||||
|         binding.ivSeekBackward10.setOnClickListener { |         binding.ivSeekBackward10.setOnClickListener { | ||||||
|             mediaController?.let { |             mediaController?.let { | ||||||
|  |                 binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle) | ||||||
|                 val sessionCommand = SessionCommand( |                 val sessionCommand = SessionCommand( | ||||||
|                     "SEEK_BACKWARD", |                     "SEEK_BACKWARD", | ||||||
|                     Bundle.EMPTY |                     Bundle.EMPTY | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ import android.content.Intent | |||||||
| import android.net.Uri | import android.net.Uri | ||||||
| import android.os.Build | import android.os.Build | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
|  | import android.os.Handler | ||||||
|  | import android.os.Looper | ||||||
| import androidx.core.os.BundleCompat | 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 | ||||||
| @@ -25,6 +27,7 @@ import com.orhanobut.logger.Logger | |||||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers | ||||||
| import io.reactivex.rxjava3.disposables.CompositeDisposable | import io.reactivex.rxjava3.disposables.CompositeDisposable | ||||||
| import io.reactivex.rxjava3.schedulers.Schedulers | 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.audio_content.playlist.detail.AudioContentPlaylistContent | ||||||
| import kr.co.vividnext.sodalive.common.Constants | import kr.co.vividnext.sodalive.common.Constants | ||||||
| import kr.co.vividnext.sodalive.common.SharedPreferenceManager | import kr.co.vividnext.sodalive.common.SharedPreferenceManager | ||||||
| @@ -45,6 +48,12 @@ class AudioContentPlayerService : MediaSessionService() { | |||||||
|  |  | ||||||
|     private val playlist = ArrayList<AudioContentPlaylistContent>() |     private val playlist = ArrayList<AudioContentPlaylistContent>() | ||||||
|  |  | ||||||
|  |     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() { |     override fun onCreate() { | ||||||
|         super.onCreate() |         super.onCreate() | ||||||
|  |  | ||||||
| @@ -66,6 +75,7 @@ class AudioContentPlayerService : MediaSessionService() { | |||||||
|  |  | ||||||
|         compositeDisposable.dispose() |         compositeDisposable.dispose() | ||||||
|         SharedPreferenceManager.isPlayerServiceRunning = false |         SharedPreferenceManager.isPlayerServiceRunning = false | ||||||
|  |         stopLoop() | ||||||
|         super.onDestroy() |         super.onDestroy() | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -83,6 +93,44 @@ class AudioContentPlayerService : MediaSessionService() { | |||||||
|         return super.onStartCommand(intent, flags, startId) |         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() { |     private fun initPlayer() { | ||||||
|         player = ExoPlayer.Builder(this).build() |         player = ExoPlayer.Builder(this).build() | ||||||
|         player!!.addListener(object : Player.Listener { |         player!!.addListener(object : Player.Listener { | ||||||
| @@ -132,6 +180,7 @@ class AudioContentPlayerService : MediaSessionService() { | |||||||
|                         .add(SessionCommand("GET_PLAYLIST", Bundle.EMPTY)) |                         .add(SessionCommand("GET_PLAYLIST", Bundle.EMPTY)) | ||||||
|                         .add(SessionCommand("SEEK_FORWARD", Bundle.EMPTY)) |                         .add(SessionCommand("SEEK_FORWARD", Bundle.EMPTY)) | ||||||
|                         .add(SessionCommand("SEEK_BACKWARD", Bundle.EMPTY)) |                         .add(SessionCommand("SEEK_BACKWARD", Bundle.EMPTY)) | ||||||
|  |                         .add(SessionCommand("TOGGLE_SEGMENT_LOOP", Bundle.EMPTY)) | ||||||
|                         .build() |                         .build() | ||||||
|  |  | ||||||
|                     return MediaSession.ConnectionResult.AcceptedResultBuilder(session) |                     return MediaSession.ConnectionResult.AcceptedResultBuilder(session) | ||||||
| @@ -147,6 +196,7 @@ class AudioContentPlayerService : MediaSessionService() { | |||||||
|                 ): ListenableFuture<SessionResult> { |                 ): ListenableFuture<SessionResult> { | ||||||
|                     return when (customCommand.customAction) { |                     return when (customCommand.customAction) { | ||||||
|                         "UPDATE_PLAYLIST" -> { |                         "UPDATE_PLAYLIST" -> { | ||||||
|  |                             stopLoop() | ||||||
|                             val playlist = BundleCompat.getParcelableArrayList( |                             val playlist = BundleCompat.getParcelableArrayList( | ||||||
|                                 args, |                                 args, | ||||||
|                                 Constants.EXTRA_AUDIO_CONTENT_PLAYLIST, |                                 Constants.EXTRA_AUDIO_CONTENT_PLAYLIST, | ||||||
| @@ -163,31 +213,66 @@ class AudioContentPlayerService : MediaSessionService() { | |||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                         "PLAY_NEXT_CONTENT" -> { |                         "PLAY_NEXT_CONTENT" -> { | ||||||
|  |                             stopLoop() | ||||||
|                             playNextContent() |                             playNextContent() | ||||||
|                             Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) |                             Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                         "PLAY_PREVIOUS_CONTENT" -> { |                         "PLAY_PREVIOUS_CONTENT" -> { | ||||||
|  |                             stopLoop() | ||||||
|                             playPreviousContent() |                             playPreviousContent() | ||||||
|                             Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) |                             Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                         "PLAY_SELECTED_CONTENT" -> { |                         "PLAY_SELECTED_CONTENT" -> { | ||||||
|  |                             stopLoop() | ||||||
|                             val selectedContentId = args.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) |                             val selectedContentId = args.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) | ||||||
|                             playSelectedContent(contentId = selectedContentId) |                             playSelectedContent(contentId = selectedContentId) | ||||||
|                             Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) |                             Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                         "SEEK_FORWARD" -> { |                         "SEEK_FORWARD" -> { | ||||||
|  |                             stopLoop() | ||||||
|                             playSeekForward() |                             playSeekForward() | ||||||
|                             Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) |                             Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                         "SEEK_BACKWARD" -> { |                         "SEEK_BACKWARD" -> { | ||||||
|  |                             stopLoop() | ||||||
|                             playSeekBackward() |                             playSeekBackward() | ||||||
|                             Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) |                             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" -> { |                         "GET_PLAYLIST" -> { | ||||||
|                             val extras = Bundle().apply { |                             val extras = Bundle().apply { | ||||||
|                                 putParcelableArrayList( |                                 putParcelableArrayList( | ||||||
|   | |||||||
| @@ -77,6 +77,7 @@ object Constants { | |||||||
|     const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview" |     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_COVER_IMAGE_URL = "audio_content_cover_image_url" | ||||||
|     const val EXTRA_AUDIO_CONTENT_PLAYLIST = "extra_audio_content_playlist" |     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 EXTRA_IS_SHOW_SECRET = "extra_is_show_secret" | ||||||
|  |  | ||||||
|     const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2 |     const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2 | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_loop_segment_active.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_loop_segment_active.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_loop_segment_idle.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_loop_segment_idle.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_loop_segment_start_set.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_loop_segment_start_set.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
| @@ -165,6 +165,16 @@ | |||||||
|         app:layout_constraintStart_toEndOf="@+id/iv_play_or_pause" |         app:layout_constraintStart_toEndOf="@+id/iv_play_or_pause" | ||||||
|         app:layout_constraintTop_toTopOf="@+id/iv_play_or_pause" /> |         app:layout_constraintTop_toTopOf="@+id/iv_play_or_pause" /> | ||||||
|  |  | ||||||
|  |     <ImageView | ||||||
|  |         android:id="@+id/iv_loop_segment" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:contentDescription="@null" | ||||||
|  |         android:padding="5dp" | ||||||
|  |         android:src="@drawable/ic_loop_segment_idle" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" /> | ||||||
|  |  | ||||||
|     <ImageView |     <ImageView | ||||||
|         android:id="@+id/iv_playlist" |         android:id="@+id/iv_playlist" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user