From bddf7b750b76507f7e2030a0e7f59bf45da5fc32 Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 1 Apr 2025 14:25:45 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B5=AC=EA=B0=84=EB=B0=98=EB=B3=B5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../player/AudioContentPlayerFragment.kt | 25 ++++++ .../player/AudioContentPlayerService.kt | 85 ++++++++++++++++++ .../co/vividnext/sodalive/common/Constants.kt | 1 + .../ic_loop_segment_active.png | Bin 0 -> 1127 bytes .../drawable-xxhdpi/ic_loop_segment_idle.png | Bin 0 -> 1034 bytes .../ic_loop_segment_start_set.png | Bin 0 -> 1165 bytes .../layout/fragment_audio_content_player.xml | 10 +++ 7 files changed, 121 insertions(+) create mode 100755 app/src/main/res/drawable-xxhdpi/ic_loop_segment_active.png create mode 100755 app/src/main/res/drawable-xxhdpi/ic_loop_segment_idle.png create mode 100755 app/src/main/res/drawable-xxhdpi/ic_loop_segment_start_set.png 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 0000000000000000000000000000000000000000..ac9aec9552378f485f92adc6d6b96c3eccb5b511 GIT binary patch literal 1127 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di49tH#T^vIy7~jtF^}FpLa{RuJmvrc=DRUM) zWJ$d_%d5rJK0theu!*hXLzWil3n0d+MNJzHPF=MnNZi-o!J@H|OEl2tvbmqku|7vW zV`0Y%`;!kOeRBSPwmZ(Iz|n+)Iy?^~FjZK)&+gf*{_D;e&xp_2Pn``EUKekk;Pg){ zDCb{<*7pNO=U8iY$%b&vdB6Pm$F^(x_Uq+0F@Cl_UVR|N^!>+4pWU7xUCg_IgZ=me z&Q&u%@7cg3+{SsaVw>=qCf7OKCn6r^K9gUvM(Z`p?B_yBh9XI3_pV5&eEynj@%VLu z;BqE6rc6CG36*`d=N9sbS+c)aX(66}f7+>kXPP&(HG1`LRh=C_Czw&PS+rKidE?S( zmZ;}U7RTa`xTr>i&)1&U6!_^8&233uu}zb-^;9Z?rwyt;F*X?;&RQYb2wJ8T)fM6^^36j)Y|MG!Ide+wJG6|w+ezE|NH5! zb?=d1aozVH$`MC8xs#8^`=778?Wlb3xW$Z&MuVFg=4N^Y&WhPD%W^MpZY?NZS6{g4 z-PY#jbE}J(bsijH3Vyfx$|~b?*T3q1)mIJAlS%#eIpvYWkyz~r@q#0@#YyLT-Cyp} zEDsJA-Oy4hE>{@O>8p9v@!G~U4W)~^{4bP0&eG1fvF2h@i09Nx@1=oK@X9{A<*vkxs6~h3wwGRg_`UmN5!?F`*1J1c zuDft9%a>%A@p|KuqqinXc*8F7DH11KlsH|J!UA@$!Z_=cnzBo7?|P-*tV@dA}mg6Anij z_ZCI9tP~B1jGMD}=RS!V)}1UzqOw-4J;(Cg|LHPWp$h`2Xu`(p%$CieG2b`u%c@aM zczk_#GPlIub%qnK8|AYsZ+qlE$KV$Xom8FT(HnaChMms1TJh}UrdE*O3o92xy zD&l4>EG8Ie56`sqkte=~=pA{t*grq2JHvG&NBa+kwadRnezMIg4w-8A_Iyk7yHnmQ hERF(jbY$*h`}JFL52k(SX#l#!Twj?1od0-M%v!M- z$$Qr0G5*7xP* zlpXyWKv-?-gQ9bO9e89@4>Ja9M3f+t1b2)k6v3{{H?9FInCDA$AWU z9*7}{L|F ziy=`3bOL4NH99{s`>(IBT`>f4uGrYvcsthD*K>`vc*~Eq9m|P}Dj)>ft~8yBXAv0O z7m18ZR2P-zpycHfRX_;z!@IoDEQmOC?zLq%H#eEYC-u&Y(CqIMO+dKGmUtb9aHqo` zOp?BTAFeZrg!TbRAA{K5B^ zVISA4izM?&jEN#3TqhIHZD0kt|2~q-$w()X zWLs>|Ny@#WoU{&(3=`LfOaBr>+7bb{N*AJxw4vj@N%uQ94lZ!r^04sR+)7B};Fi0m zi@sc871R?0qAcYQe{|`VHu=mn@u|9$gE*Vx*1MWaG#+|Q#mdUcrWj!E!d3UJ-4Q!C z{trL?I#Myh+=Z*|3#;fO=3R}u*VTy~cfJGhdzLj?iL*ez$?P}G1%!CjO#B#h?m7~fdCT-VpW7SMR9k^HZ zmz=-%8E2ij*VI7_wN56(%>+!&foAEowY9zw^6{|VJ)T!I6g3%Ztr&sII=1-ft`jV%!C;v+G9C^KhtntE7c z0r3nY%72aL0rwETk$`mJKqz1!6fh797zhOngaQUa0Ry3clc}{>sQXmz$ek98T0}ck zv_elR|Kkql`uaLu^k>2)I#wZs5JCtcgb+dqA!PCR2MX$kD~X=(+W-In07*qoM6N<$ Eg1hV40{{R3 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2703ce554aaef66f337afc9cfa56b75fa6550cc6 GIT binary patch literal 1165 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBu9ByVV@L(#+ga!Rg&jqX-_Ke7FeNIAZ;p<+ z|EY^2!d+@^eh10f+4AUJHQ)cv_@2hd(WHQalz1hUI8^>#a!#hqeeUjWccTl$KOJA- zxccfUW3hj`n?KmR4>p^<_W9?ZY7)nGHI#3^wY2B)8~5isAFO^a_w@CGgBLGe)Y}*G zXkLHP^?)O#1|7`P3eWvymsK}r*Hmsnz=bb^n!Rtr&qxl$(DEf9y^HgG!?G1w6nV>ot&Jk$F;3Ne&t59>v^+x z7lyH>@I1Sk{P3f%z?rx8^JcQ0PB=69(OcJLTPIELN@bk;^dIA!z4OnSw|jKAf^s>F?gWlAn||-uB6!fjE{TWdP>1&9uAc4$fo!Hl9$7Wn zbrmz-A6LA%@tEFHkvliaBYlj_4;qBAOley2t~+bjLU~cyY@P=bQx-Sw@my!rG<(|N zK*#Q!1>W||%_#?&4i&DtxbsukZm~VbEhMxW4Q_gvUyFNiO4WNy%w%sd5x-*xPrkDZ z75TUzK;y~*hA^+4pU$1M<4)gKAJ6gJI6v)k-lX>~-j*WiNdbGcCwKO=p9(21DJfy% zNx0K`en-9!&#sS>0aspFZD46Lcy}oMPPl_JN1efom}v(~7kzj<`Ten}y=A`JosB!z zv91a}R(H6DC&hsM)YiD)2gA<`r)K>=y(Q??(|sP$J3t)YfQGyFq39h*40{5YOZJX z_m@Y7W%D)b{=7NOr-QqCG>*^ttU9?jG?^)^?6xJF(i9GxbyxN>7o=aXn)-}s(r?ow zo0&h(vTQk0I&*3F(IjS$NB<9(eE+ekDbhOduxS{N&ik6!$+WoDX3BCxMU!^Ozc7e6 zxNrW)bst^t>|^}!LyY})!fNey?iViiJ#`lR-{Gd!pZe|RAKnL{+z%WB4}>vG#cZa@n%e2%opE + +