Retain ringtone resources and retry cleanup before replacement
This commit is contained in:
1 parent
6abedd8177
commit
94b9786d91
2 files changed
+78
-43
No files matched your search
@@ -83,7 +83,6 @@ object IncomingCallManager {
|
||||
checkMain()
|
||||
val previous = active
|
||||
active = null
|
||||
if (previous != null) ringtone.stop()
|
||||
cleanupStopped(forceNotification = previous != null)
|
||||
if (previous != null) {
|
||||
LogManager.addLog("INFO", "InCall", "ringing stopped callId=${previous.callId} radio=${previous.radio} " +
|
||||
@@ -93,6 +92,7 @@ object IncomingCallManager {
|
||||
|
||||
/** Только без active: не теряет ссылки при отказе и не повторяет уже подтверждённую очистку. */
|
||||
private fun cleanupStopped(forceNotification: Boolean) {
|
||||
ringtone.stop()
|
||||
releaseWakeLock()
|
||||
for ((token, owner) in pendingIntents.toList()) {
|
||||
try {
|
||||
|
||||
@@ -30,15 +30,20 @@ import com.utun.chat.R
|
||||
*
|
||||
* API 28+ : звук через Ringtone (RingtoneManager), громкость — «громкость звонка».
|
||||
* API 26–27: MediaPlayer(STREAM_RING, loop) только после получения audio focus.
|
||||
* Потеря focus гасит звук до следующего play, сохраняя вибрацию; stop освобождает оба эффекта.
|
||||
* Потеря focus запрашивает остановку звука до следующего play, сохраняя вибрацию.
|
||||
* Stop сразу отключает callbacks/таймер; ссылки очищаются только после успешных stop/release/cancel/abandon.
|
||||
* Повтор stop или начало play повторяет незавершённую очистку. Новый звук/вибрация не заменяет неочищенного владельца.
|
||||
* Ошибка очистки звука/focus не блокирует вибрацию и наоборот. Таймера повторной очистки нет.
|
||||
*/
|
||||
class RingtonePlayer {
|
||||
|
||||
private var ringtone: Ringtone? = null
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private var vibrator: Vibrator? = null
|
||||
private var focusRequest: AudioFocusRequest? = null
|
||||
private var focusManager: AudioManager? = null
|
||||
private var ringtone: Ringtone? = null // Текущий либо ожидающий подтверждения stop плеер API 28+.
|
||||
private var mediaPlayer: MediaPlayer? = null // Текущий либо ожидающий подтверждения release плеер API 26–27.
|
||||
private var vibrator: Vibrator? = null // Сохраняется при отказе cancel, даже после снятия таймера.
|
||||
private var focusRequest: AudioFocusRequest? = null // Сохраняется до AUDIOFOCUS_REQUEST_GRANTED от abandon.
|
||||
private var focusManager: AudioManager? = null // Исходный менеджер соответствующего focusRequest.
|
||||
private var soundActive = false // При очистке callbacks выключены, хотя ссылки ещё могут оставаться.
|
||||
private var vibrating = false // Отдельно от сохранённого сервиса: false запрещает новые импульсы.
|
||||
private val attributes = AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build()
|
||||
|
||||
@@ -47,6 +52,7 @@ class RingtonePlayer {
|
||||
private val vibrateHandler = Handler(Looper.getMainLooper())
|
||||
private val vibrateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (!vibrating) return
|
||||
val v = vibrator ?: return
|
||||
try {
|
||||
v.vibrate(VibrationEffect.createWaveform(longArrayOf(0, 1000), -1), attributes)
|
||||
@@ -55,13 +61,14 @@ class RingtonePlayer {
|
||||
stopVibrate()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "Ringtone", "vibration pulse failed: ${e.message}")
|
||||
LogManager.addLog("WARN", "Ringtone", "vibration pulse failed: $e")
|
||||
stopVibrate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** На main заменить прежнее оповещение мелодией resourceId и вибрацией согласно режиму ОС. */
|
||||
/** На main очистить прежнее оповещение, затем запустить resourceId/вибрацию согласно режиму ОС.
|
||||
* Пока очистка звука/focus либо вибратора не подтверждена, соответствующий новый эффект пропускается с WARN. */
|
||||
fun play(@RawRes resourceId: Int = R.raw.ringtone) {
|
||||
stop()
|
||||
val ctx = ChatApplication.instance
|
||||
@@ -71,7 +78,7 @@ class RingtonePlayer {
|
||||
am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
ringerMode = am.ringerMode
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "Ringtone", "ringer mode unavailable: ${e.message}")
|
||||
LogManager.addLog("WARN", "Ringtone", "ringer mode unavailable: $e")
|
||||
return
|
||||
}
|
||||
if (ringerMode !in AudioManager.RINGER_MODE_SILENT..AudioManager.RINGER_MODE_NORMAL) {
|
||||
@@ -89,53 +96,62 @@ class RingtonePlayer {
|
||||
}
|
||||
|
||||
private fun playSound(@RawRes resourceId: Int, am: AudioManager) {
|
||||
if (ringtone != null || mediaPlayer != null || focusRequest != null) {
|
||||
LogManager.addLog("WARN", "Ringtone", "sound skipped: cleanup pending resource=$resourceId " +
|
||||
"ringtone=${ringtone != null} player=${mediaPlayer != null} focus=${focusRequest != null}")
|
||||
return
|
||||
}
|
||||
val ctx = ChatApplication.instance
|
||||
soundActive = true
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val uri = Uri.parse("android.resource://${ctx.packageName}/$resourceId")
|
||||
val r = RingtoneManager.getRingtone(ctx, uri)
|
||||
if (r == null) {
|
||||
LogManager.addLog("ERROR", "Ringtone", "getRingtone returned null resource=$resourceId")
|
||||
stopSound()
|
||||
return
|
||||
}
|
||||
ringtone = r
|
||||
r.audioAttributes = attributes
|
||||
r.isLooping = true
|
||||
r.play()
|
||||
LogManager.addLog("INFO", "Ringtone", "sound started (Ringtone, STREAM_RING)")
|
||||
LogManager.addLog("INFO", "Ringtone", "sound started (Ringtone, STREAM_RING) resource=$resourceId player=${System.identityHashCode(r)}")
|
||||
} else {
|
||||
if (!requestAudioFocus(am)) return
|
||||
if (!requestAudioFocus(am)) { stopSound(); return }
|
||||
val mp = MediaPlayer.create(ctx, resourceId, attributes, AudioManager.AUDIO_SESSION_ID_GENERATE)
|
||||
if (mp == null) {
|
||||
LogManager.addLog("ERROR", "Ringtone", "MediaPlayer.create returned null resource=$resourceId")
|
||||
abandonAudioFocus()
|
||||
stopSound()
|
||||
return
|
||||
}
|
||||
mediaPlayer = mp
|
||||
mp.setOnErrorListener { failed, what, extra ->
|
||||
if (mediaPlayer === failed) {
|
||||
if (soundActive && mediaPlayer === failed) {
|
||||
LogManager.addLog("ERROR", "Ringtone", "playback error what=$what extra=$extra")
|
||||
stopSound()
|
||||
}
|
||||
true
|
||||
}
|
||||
mp.setOnCompletionListener { completed ->
|
||||
if (mediaPlayer === completed) {
|
||||
if (soundActive && mediaPlayer === completed) {
|
||||
LogManager.addLog("WARN", "Ringtone", "loop unexpectedly completed")
|
||||
stopSound()
|
||||
}
|
||||
}
|
||||
mp.isLooping = true
|
||||
mp.start()
|
||||
LogManager.addLog("INFO", "Ringtone", "sound started (MediaPlayer fallback, STREAM_RING)")
|
||||
LogManager.addLog("INFO", "Ringtone", "sound started (MediaPlayer fallback, STREAM_RING) " +
|
||||
"resource=$resourceId player=${System.identityHashCode(mp)}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "Ringtone", "sound failed resource=$resourceId: ${e.message}")
|
||||
LogManager.addLog("ERROR", "Ringtone", "sound failed resource=$resourceId: $e")
|
||||
stopSound()
|
||||
}
|
||||
}
|
||||
|
||||
/** На main остановить звук, focus и таймер вибрации. Повторный вызов не обращается к старым ресурсам. */
|
||||
/** На main отключить callbacks/таймер и независимо очистить звук, focus и вибратор.
|
||||
* Ошибка сохраняет владельца для следующего stop/play; успешно очищенный ресурс повторно не вызывается. */
|
||||
fun stop() {
|
||||
check(Looper.myLooper() == Looper.getMainLooper()) {
|
||||
LogManager.addLog("ERROR", "Ringtone", "operation requires main thread")
|
||||
@@ -144,19 +160,27 @@ class RingtonePlayer {
|
||||
val active = ringtone != null || mediaPlayer != null || vibrator != null || focusRequest != null
|
||||
stopSound()
|
||||
stopVibrate()
|
||||
if (active) LogManager.addLog("INFO", "Ringtone", "stopped")
|
||||
if (active) LogManager.addLog("INFO", "Ringtone", "stop requested sound_pending=${ringtone != null || mediaPlayer != null} " +
|
||||
"focus_pending=${focusRequest != null} vibration_pending=${vibrator != null}")
|
||||
}
|
||||
|
||||
private fun stopSound() {
|
||||
soundActive = false
|
||||
val r = ringtone
|
||||
ringtone = null
|
||||
try { r?.stop() } catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "Ringtone", "ringtone stop failed: ${e.message}")
|
||||
if (r != null) try {
|
||||
r.stop()
|
||||
ringtone = null
|
||||
LogManager.addLog("DEBUG", "Ringtone", "ringtone stopped player=${System.identityHashCode(r)}")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "Ringtone", "ringtone stop failed player=${System.identityHashCode(r)}; retained for retry: $e")
|
||||
}
|
||||
val mp = mediaPlayer
|
||||
mediaPlayer = null
|
||||
try { mp?.release() } catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "Ringtone", "player release failed: ${e.message}")
|
||||
if (mp != null) try {
|
||||
mp.release()
|
||||
mediaPlayer = null
|
||||
LogManager.addLog("DEBUG", "Ringtone", "player released player=${System.identityHashCode(mp)}")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "Ringtone", "player release failed player=${System.identityHashCode(mp)}; retained for retry: $e")
|
||||
}
|
||||
abandonAudioFocus()
|
||||
}
|
||||
@@ -164,6 +188,10 @@ class RingtonePlayer {
|
||||
/* ── вибрация (решение принято в play(): вибрируем всегда, кроме SILENT) ── */
|
||||
|
||||
private fun startVibrate() {
|
||||
if (vibrator != null) {
|
||||
LogManager.addLog("WARN", "Ringtone", "vibration skipped: cleanup pending vibrator=${System.identityHashCode(vibrator)}")
|
||||
return
|
||||
}
|
||||
try {
|
||||
val v = ChatApplication.instance.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
if (!v.hasVibrator()) {
|
||||
@@ -171,22 +199,26 @@ class RingtonePlayer {
|
||||
return
|
||||
}
|
||||
vibrator = v
|
||||
vibrating = true
|
||||
vibrateRunnable.run()
|
||||
if (vibrator === v) LogManager.addLog("INFO", "Ringtone", "vibration started")
|
||||
if (vibrating) LogManager.addLog("INFO", "Ringtone", "vibration started vibrator=${System.identityHashCode(v)}")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "Ringtone", "vibrator unavailable: ${e.message}")
|
||||
LogManager.addLog("WARN", "Ringtone", "vibrator unavailable: $e")
|
||||
stopVibrate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVibrate() {
|
||||
val v = vibrator
|
||||
vibrator = null
|
||||
vibrating = false
|
||||
vibrateHandler.removeCallbacks(vibrateRunnable)
|
||||
try { v?.cancel() } catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "Ringtone", "vibration cancel failed: ${e.message}")
|
||||
val v = vibrator ?: return
|
||||
try {
|
||||
v.cancel()
|
||||
vibrator = null
|
||||
LogManager.addLog("INFO", "Ringtone", "vibration stopped vibrator=${System.identityHashCode(v)}")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "Ringtone", "vibration cancel failed vibrator=${System.identityHashCode(v)}; retained for retry: $e")
|
||||
}
|
||||
if (v != null) LogManager.addLog("INFO", "Ringtone", "vibration stopped")
|
||||
}
|
||||
|
||||
/* ── audio focus (fallback API 26–27) ── */
|
||||
@@ -195,8 +227,8 @@ class RingtonePlayer {
|
||||
lateinit var req: AudioFocusRequest
|
||||
req = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT).setAudioAttributes(attributes)
|
||||
.setOnAudioFocusChangeListener({ change ->
|
||||
if (focusRequest === req && change < 0) {
|
||||
LogManager.addLog("INFO", "Ringtone", "audio focus lost change=$change; sound stopped")
|
||||
if (soundActive && focusRequest === req && change < 0) {
|
||||
LogManager.addLog("INFO", "Ringtone", "audio focus lost change=$change; stopping sound")
|
||||
stopSound()
|
||||
}
|
||||
}, vibrateHandler).build()
|
||||
@@ -205,24 +237,27 @@ class RingtonePlayer {
|
||||
val result = am.requestAudioFocus(req)
|
||||
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
LogManager.addLog("WARN", "Ringtone", "audio focus denied result=$result; sound suppressed")
|
||||
abandonAudioFocus()
|
||||
return false
|
||||
}
|
||||
LogManager.addLog("DEBUG", "Ringtone", "audio focus granted")
|
||||
LogManager.addLog("DEBUG", "Ringtone", "audio focus granted request=${System.identityHashCode(req)}")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun abandonAudioFocus() {
|
||||
val req = focusRequest ?: return
|
||||
val am = focusManager
|
||||
focusRequest = null
|
||||
focusManager = null
|
||||
try {
|
||||
val result = am?.abandonAudioFocusRequest(req)
|
||||
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
||||
LogManager.addLog("WARN", "Ringtone", "abandon focus rejected result=$result")
|
||||
val am = requireNotNull(focusManager) { "audio focus manager missing" }
|
||||
val result = am.abandonAudioFocusRequest(req)
|
||||
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
LogManager.addLog("WARN", "Ringtone", "abandon focus rejected result=$result " +
|
||||
"request=${System.identityHashCode(req)}; retained for retry")
|
||||
return
|
||||
}
|
||||
focusRequest = null
|
||||
focusManager = null
|
||||
LogManager.addLog("DEBUG", "Ringtone", "audio focus abandoned request=${System.identityHashCode(req)}")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "Ringtone", "abandon focus failed: ${e.message}")
|
||||
LogManager.addLog("WARN", "Ringtone", "abandon focus failed request=${System.identityHashCode(req)}; retained for retry: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user