Keep notification players strongly owned throughout playback

This commit is contained in:
evgeny committed 2026-10-28 14:43:27 +01:00
1 parent b318627022
commit 776345997f
2 files changed
+11 -10

No files matched your search

@@ -47,7 +47,7 @@ object CallRingback {
if (active && resourceId == resId) return
release("phase changed")
if (player != null) {
LogManager.addLog("WARN", "Ringback", "start skipped phase=$tag resource=$resId: cleanup pending " +
LogManager.addLog("WARN", "Ringback", "start skipped phase=$tag requested_resource=$resId: cleanup pending " +
"resource=$resourceId player=${System.identityHashCode(player)}")
return
}
@@ -10,6 +10,7 @@ import com.utun.chat.R
/**
* Однократный msg_incoming.mp3 через профиль уведомлений. play вызывается на main.
* enabled и mutedChannels — снимки настроек; их меняет main. Независимые звуки могут перекрываться.
* Владелец хранит сильную ссылку на каждый плеер до успешного release, включая время проигрывания.
* Каждый плеер получает release по completion/error или при ошибке настройки/запуска.
* Ошибка сохраняет владельца для следующего callback/play; успешная очистка повторно не вызывается.
* Play сначала повторяет отложенную очистку, даже при mute/disabled или отказе создания нового звука.
@@ -19,9 +20,9 @@ object NotificationSound {
@Volatile var enabled: Boolean = true
@Volatile var mutedChannels: Set<String> = emptySet()
/** Владелец отдельного звука; released общий для callbacks и повторной очистки перед новым play. */
private class Playback(val player: MediaPlayer, val channelId: String) { var released = false }
private val pending = mutableSetOf<Playback>() // Только плееры с неподтверждённым release; доступ на main.
/** Один звук; cleanupPending отличает неудачный release от ещё играющего плеера. */
private class Playback(val player: MediaPlayer, val channelId: String) { var cleanupPending = false }
private val playbacks = mutableSetOf<Playback>() // Сильные ссылки до успешного release; доступ только на main.
/** Прочитать снимок настройки mute канала/личной беседы (DM использует ID с префиксом dm:). */
fun isMuted(channelId: String): Boolean = channelId in mutedChannels
@@ -37,7 +38,7 @@ object NotificationSound {
LogManager.addLog("ERROR", "Sound", "play requires main thread")
"NotificationSound requires main thread"
}
for (playback in pending.toList()) release(playback, "retry before play")
for (playback in playbacks.filter { it.cleanupPending }) release(playback, "retry before play")
if (!enabled || isMuted(channelId)) return
val mp = try {
val attrs = AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION)
@@ -52,10 +53,11 @@ object NotificationSound {
return
}
val playback = Playback(mp, channelId)
playbacks.add(playback)
try {
mp.setOnCompletionListener { release(playback, "completed") }
mp.setOnErrorListener { _, what, extra ->
if (!playback.released) LogManager.addLog("WARN", "Sound", "playback error ch=$channelId what=$what extra=$extra")
if (playback in playbacks) LogManager.addLog("WARN", "Sound", "playback error ch=$channelId what=$what extra=$extra")
release(playback, "error")
true
}
@@ -69,16 +71,15 @@ object NotificationSound {
/** Один release независимо от остальных звуков; исключение сохраняет владельца для следующего callback/play. */
private fun release(playback: Playback, reason: String) {
if (playback.released) return
if (playback !in playbacks) return
val mp = playback.player
try { mp.release() } catch (e: Exception) {
pending.add(playback)
playback.cleanupPending = true
LogManager.addLog("WARN", "Sound", "release failed ch=${playback.channelId} reason=$reason " +
"player=${System.identityHashCode(mp)}; retained for retry: $e")
return
}
playback.released = true
pending.remove(playback)
playbacks.remove(playback)
LogManager.addLog("DEBUG", "Sound", "notification released ch=${playback.channelId} reason=$reason " +
"player=${System.identityHashCode(mp)}")
}