구간반복 기능 추가
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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<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() { | ||||
|         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<SessionResult> { | ||||
|                     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( | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										
											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_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 | ||||
|         android:id="@+id/iv_playlist" | ||||
|         android:layout_width="wrap_content" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user