Звонки: надёжность отмены (INVITE-кодограмма + watchdog) и вибрация входящего по ringer-режиму

This commit is contained in:
evgeny committed 2026-09-24 10:15:19 +03:00
1 parent b90dc752d2
commit cc269e810c
5 files changed
+144 -71

No files matched your search

@@ -16,12 +16,18 @@ import com.utun.chat.ChatApplication
import com.utun.chat.R
/**
* Мелодия входящего звонка через профиль звонка (STREAM_RING):
* - громкость — настройка «громкость вызова»;
* - вибрация — по системной настройке «вибрировать при вызове» (VIBRATE_WHEN_RINGING).
* Мелодия входящего звонка через профиль звонка (STREAM_RING) + ручная вибрация.
*
* API 28+ : Ringtone (RingtoneManager) — громкость и вибрация автоматически.
* API 26–27: MediaPlayer(STREAM_RING, loop) + ручная вибрация по системной настройке.
* Поведение по системному режиму звонка (AudioManager.getRingerMode):
* - NORMAL : звук + вибрация, если включена настройка «вибрировать при вызове»
* (Settings.System.VIBRATE_WHEN_RINGING);
* - VIBRATE: без звука, только вибрация;
* - SILENT : без звука и вибрации.
*
* Вибрация всегда запускается вручную через Vibrator (класс Ringtone сам не вибрирует).
*
* API 28+ : звук через Ringtone (RingtoneManager), громкость — «громкость вызова».
* API 26–27: звук через MediaPlayer(STREAM_RING, loop) + audio focus.
*/
class RingtonePlayer {
@@ -33,50 +39,69 @@ class RingtonePlayer {
fun play() {
stop()
val ctx = ChatApplication.instance
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val uri = Uri.parse("android.resource://${ctx.packageName}/${R.raw.ringtone}")
val r = try {
RingtoneManager.getRingtone(ctx, uri)
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "getRingtone failed: ${e.message}")
null
} ?: return
try {
r.isLooping = true
r.play()
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "play failed: ${e.message}")
return
val am = try {
ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
} catch (e: Exception) {
LogManager.addLog("WARN", "Ringtone", "audio service unavailable: ${e.message}")
null
} ?: return
val ringerMode = am.ringerMode
val playSound = ringerMode == AudioManager.RINGER_MODE_NORMAL
val playVibrate = ringerMode == AudioManager.RINGER_MODE_VIBRATE
|| (playSound && systemVibrateWhenRinging())
LogManager.addLog("INFO", "Ringtone",
"ringerMode=%d sound=%b vibrate=%b".format(ringerMode, playSound, playVibrate))
if (playSound) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val uri = Uri.parse("android.resource://${ctx.packageName}/${R.raw.ringtone}")
val r = try {
RingtoneManager.getRingtone(ctx, uri)
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "getRingtone failed: ${e.message}")
null
} ?: return
try {
r.isLooping = true
r.play()
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "play failed: ${e.message}")
return
}
ringtone = r
LogManager.addLog("INFO", "Ringtone", "sound started (Ringtone, STREAM_RING)")
} else {
val mp = try {
MediaPlayer.create(ctx, R.raw.ringtone)
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "MediaPlayer.create failed: ${e.message}")
null
} ?: return
try {
mp.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setLegacyStreamType(AudioManager.STREAM_RING)
.build()
)
mp.isLooping = true
requestAudioFocus()
mp.start()
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "MediaPlayer start failed: ${e.message}")
try { mp.release() } catch (_: Exception) {}
abandonAudioFocus()
return
}
mediaPlayer = mp
LogManager.addLog("INFO", "Ringtone", "sound started (MediaPlayer fallback, STREAM_RING)")
}
ringtone = r
LogManager.addLog("INFO", "Ringtone", "started (Ringtone, STREAM_RING + system vibrate)")
} else {
val mp = try {
MediaPlayer.create(ctx, R.raw.ringtone)
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "MediaPlayer.create failed: ${e.message}")
null
} ?: return
try {
mp.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setLegacyStreamType(AudioManager.STREAM_RING)
.build()
)
mp.isLooping = true
requestAudioFocus()
mp.start()
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "MediaPlayer start failed: ${e.message}")
try { mp.release() } catch (_: Exception) {}
abandonAudioFocus()
return
}
mediaPlayer = mp
}
if (playVibrate) {
startVibrate()
LogManager.addLog("INFO", "Ringtone", "started (MediaPlayer fallback, STREAM_RING)")
}
}
@@ -95,7 +120,7 @@ class RingtonePlayer {
LogManager.addLog("INFO", "Ringtone", "stopped")
}
/* ── вибрация (fallback API 26–27) по системной настройке ── */
/* ── вибрация (решение принято в play(): VIBRATE-режим или VIBRATE_WHEN_RINGING) ── */
private fun systemVibrateWhenRinging(): Boolean = try {
Settings.System.getInt(
@@ -105,7 +130,6 @@ class RingtonePlayer {
}
private fun startVibrate() {
if (!systemVibrateWhenRinging()) return
val v = try {
ChatApplication.instance.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
} catch (e: Exception) {
@@ -116,6 +140,7 @@ class RingtonePlayer {
try {
v.vibrate(VibrationEffect.createWaveform(longArrayOf(1000, 1000), 0))
vibrator = v
LogManager.addLog("INFO", "Ringtone", "vibration started")
} catch (e: Exception) {
LogManager.addLog("WARN", "Ringtone", "vibrate failed: ${e.message}")
}