From e8b8941f8a03feae623ba66b9f47dc40c80d3d5f Mon Sep 17 00:00:00 2001 From: gagip Date: Thu, 26 Mar 2026 02:17:06 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=9E=AC=EC=83=9D=20=ED=81=90=20?= =?UTF-8?q?=EB=B0=8F=20=EC=98=A4=EB=94=94=EC=98=A4=20=ED=8F=AC=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlayQueueManager.playNext(): check()를 mutex 내부로 이동하여 check와 실행 사이 경쟁 조건 제거 - AudioFocusManager: wasPlayingBeforeTransientLoss, wasDucked 필드에 @Volatile 추가로 콜백 스레드 가시성 보장 --- .../zenplayer/features/player/data/AudioFocusManager.kt | 4 ++++ .../zenplayer/features/player/data/PlayQueueManager.kt | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/happyseal/zenplayer/features/player/data/AudioFocusManager.kt b/app/src/main/java/com/happyseal/zenplayer/features/player/data/AudioFocusManager.kt index cfa91350..be0d5387 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/player/data/AudioFocusManager.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/player/data/AudioFocusManager.kt @@ -40,7 +40,11 @@ class AudioFocusManager( private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager private var currentFocusState = FocusState.NO_FOCUS + + @Volatile private var wasPlayingBeforeTransientLoss = false + + @Volatile private var wasDucked = false private val audioFocusChangeListener = diff --git a/app/src/main/java/com/happyseal/zenplayer/features/player/data/PlayQueueManager.kt b/app/src/main/java/com/happyseal/zenplayer/features/player/data/PlayQueueManager.kt index d21ddd51..ae11a2c3 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/player/data/PlayQueueManager.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/player/data/PlayQueueManager.kt @@ -84,8 +84,10 @@ class PlayQueueManager( * 다음 곡 재생 */ override suspend fun playNext() { - check(_playQueue.value != null) { "재생 큐가 설정되지 않은 상태에서 playNext() 호출" } - playlistMutex.withLock { playNextInternal() } + playlistMutex.withLock { + check(_playQueue.value != null) { "재생 큐가 설정되지 않은 상태에서 playNext() 호출" } + playNextInternal() + } } /** From 50fb23199d0742c8ca25419d0021b2bad23a54fb Mon Sep 17 00:00:00 2001 From: gagip Date: Thu, 26 Mar 2026 09:12:24 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20mutex=20=EB=9D=BD=20=EB=B3=B4?= =?UTF-8?q?=EC=9C=A0=20=EC=A4=91=20suspend=20=EB=B0=8F=20currentFocusState?= =?UTF-8?q?=20=EA=B0=80=EC=8B=9C=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlayQueueManager: playNext()에서 락 안에 suspend 함수 직접 호출 시 교착 가능성 제거 - computeNextAction()으로 분리하여 상태 변경은 락 안에서, emit/play는 락 밖에서 실행 - AudioFocusManager: currentFocusState에 @Volatile 추가하여 스레드 간 가시성 보장 --- .../features/player/data/AudioFocusManager.kt | 1 + .../features/player/data/PlayQueueManager.kt | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/happyseal/zenplayer/features/player/data/AudioFocusManager.kt b/app/src/main/java/com/happyseal/zenplayer/features/player/data/AudioFocusManager.kt index be0d5387..72db6672 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/player/data/AudioFocusManager.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/player/data/AudioFocusManager.kt @@ -39,6 +39,7 @@ class AudioFocusManager( ) { private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + @Volatile private var currentFocusState = FocusState.NO_FOCUS @Volatile diff --git a/app/src/main/java/com/happyseal/zenplayer/features/player/data/PlayQueueManager.kt b/app/src/main/java/com/happyseal/zenplayer/features/player/data/PlayQueueManager.kt index ae11a2c3..6f18942b 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/player/data/PlayQueueManager.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/player/data/PlayQueueManager.kt @@ -84,10 +84,11 @@ class PlayQueueManager( * 다음 곡 재생 */ override suspend fun playNext() { - playlistMutex.withLock { + val action = playlistMutex.withLock { check(_playQueue.value != null) { "재생 큐가 설정되지 않은 상태에서 playNext() 호출" } - playNextInternal() + computeNextAction() } + action() } /** @@ -134,30 +135,33 @@ class PlayQueueManager( } } - private suspend fun playNextInternal() { - val current = _playQueue.value ?: return + // 락 안에서 상태를 변경하고, 락 밖에서 실행할 side effect(emit/play)를 반환한다. + private fun computeNextAction(): suspend () -> Unit { + val current = _playQueue.value ?: return {} - when (_repeatMode.value) { - RepeatMode.ONE -> { - current.currentTrack?.let { audioController.prepareTrack(it) } + if (_repeatMode.value == RepeatMode.ONE) { + val track = current.currentTrack + return { + track?.let { audioController.prepareTrack(it) } audioController.play() - return } - // NONE/ALL은 공통 로직(다음 인덱스 계산)으로 fall-through - RepeatMode.NONE, - RepeatMode.ALL, - -> Unit } val nextIndex = getNextIndex(current) - if (nextIndex != null) { + return if (nextIndex != null) { val updated = current.copy(currentIndex = nextIndex) _playQueue.value = updated - updated.currentTrack?.let { audioController.prepareTrack(it) } - audioController.play() + val track = updated.currentTrack + { + track?.let { audioController.prepareTrack(it) } + audioController.play() + } } else { - _events.emit(PlaylistEvent.PlaylistEnded) - current.currentTrack?.let { audioController.prepareTrack(it) } + val track = current.currentTrack + { + _events.emit(PlaylistEvent.PlaylistEnded) + track?.let { audioController.prepareTrack(it) } + } } }