diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallRingback.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallRingback.kt index 47b32dc3..662f1f60 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallRingback.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallRingback.kt @@ -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 } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NotificationSound.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NotificationSound.kt index 0b097629..7cba8135 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NotificationSound.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NotificationSound.kt @@ -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 = emptySet() - /** Владелец отдельного звука; released общий для callbacks и повторной очистки перед новым play. */ - private class Playback(val player: MediaPlayer, val channelId: String) { var released = false } - private val pending = mutableSetOf() // Только плееры с неподтверждённым release; доступ на main. + /** Один звук; cleanupPending отличает неудачный release от ещё играющего плеера. */ + private class Playback(val player: MediaPlayer, val channelId: String) { var cleanupPending = false } + private val playbacks = mutableSetOf() // Сильные ссылки до успешного 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)}") }