call: две фазы исходящего вызова — индикация + разные гудки (desktop/Android)

This commit is contained in:
evgeny committed 2026-09-23 19:16:53 +03:00
1 parent 7198154106
commit b90dc752d2
21 files changed
+170 -21

No files matched your search

@@ -113,7 +113,7 @@ class MainActivity : ComponentActivity() {
if (screen != Screen.Call) screenBeforeCall = screen
screen = Screen.Call
}
null -> if (screen == Screen.Call) screen = Screen.ChannelList
null -> if (screen == Screen.Call) screen = screenBeforeCall ?: Screen.ChannelList
else -> {}
}
}
@@ -254,7 +254,7 @@ class MainActivity : ComponentActivity() {
Screen.Logs -> LogScreen(onBack = { screen = Screen.ChannelList })
Screen.Call -> CallScreen(
viewModel = vm,
onClose = { vm.dismissCall(); screen = Screen.ChannelList },
onClose = { vm.dismissCall(); screen = screenBeforeCall ?: Screen.ChannelList },
onMinimize = { screen = screenBeforeCall ?: Screen.ChannelList }
)
Screen.QrScan -> {
@@ -0,0 +1,72 @@
package com.utun.chat.data
import android.media.AudioAttributes
import android.media.MediaPlayer
import com.utun.chat.ChatApplication
import com.utun.chat.R
/**
* Гудки исходящего звонка (ringback) для вызывающего через зацикленный MediaPlayer.
*
* Две фазы с разными гудками:
* - фаза 1 «пытаемся связаться» — call_connecting (короткие бипы 425 Гц);
* - фаза 2 «звоним» (пир принял вызов, у него звонит) — call_ringing (длинные гудки).
*
* ToneGenerator для call-тонов на современном Android (API 31+) молчит, поэтому
* гудки — готовые MP3 из res/raw, как входящая мелодия (RingtonePlayer).
* На ACCEPTED/ENDED/hangup вызывающий останавливает гудок. Идемпотентный stop.
*/
object CallRingback {
@Volatile private var player: MediaPlayer? = null
/** Фаза 1: короткие бипы «устанавливаем соединение». Идемпотентно. */
fun startConnecting() {
start(R.raw.call_connecting, "connecting")
}
/** Фаза 2: длинные гудки (у пира идёт вызов). Идемпотентно. */
fun startRinging() {
start(R.raw.call_ringing, "ringing")
}
/** Остановить текущий гудок. Идемпотентно. */
fun stop() {
release()
LogManager.addLog("INFO", "Ringback", "stopped")
}
private fun start(resId: Int, tag: String) {
release()
val ctx = ChatApplication.instance
val mp = try {
MediaPlayer.create(ctx, resId)
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringback", "MediaPlayer.create failed ($tag): ${e.message}")
null
} ?: return
try {
mp.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
mp.isLooping = true
mp.start()
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringback", "MediaPlayer start failed ($tag): ${e.message}")
try { mp.release() } catch (_: Exception) {}
return
}
player = mp
LogManager.addLog("INFO", "Ringback", "started $tag")
}
private fun release() {
val p = player ?: return
player = null
try { p.stop() } catch (_: Exception) {}
try { p.release() } catch (_: Exception) {}
}
}
@@ -44,8 +44,8 @@ fun ActiveCallBanner(
val name = st.peerName.ifEmpty { "0x%016x".format(st.peerNodeId) }
val status = when (phase) {
CallPhase.ACTIVE -> "Разговор " + fmtDuration(st.durationMs)
CallPhase.RINGING -> "Гудки…"
else -> "Звоним…"
CallPhase.RINGING -> "Звоним…"
else -> "Пытаемся связаться…"
}
Row(
@@ -40,8 +40,8 @@ private fun fmtDuration(sec: Int): String {
}
private fun callStatusText(phase: CallPhase): String = when (phase) {
CallPhase.OUTGOING -> "Звоним…"
CallPhase.RINGING -> "Гудки…"
CallPhase.OUTGOING -> "Пытаемся связаться…"
CallPhase.RINGING -> "Звоним…"
CallPhase.ACTIVE -> "Разговор"
CallPhase.ENDED -> "Завершён"
else -> ""
@@ -574,6 +574,7 @@ class ChatViewModel : ViewModel() {
val st = _callState.value
if (st != null && st.callId == callId) {
_callState.value = st.copy(phase = CallPhase.RINGING)
CallRingback.startRinging()
LogManager.addLog("INFO", "VM", "CALL_RINGING id=0x%016x".format(callId))
}
}
@@ -583,6 +584,7 @@ class ChatViewModel : ViewModel() {
val st = _callState.value
if (st != null && st.callId == callId) {
LogManager.addLog("INFO", "VM", "CALL_ACCEPTED id=0x%016x".format(callId))
CallRingback.stop()
callAudio.router.setPreferredRoute(
ChatApplication.instance.configProvider.getCallAudioRoute())
if (!callAudio.start(callId)) {
@@ -666,6 +668,7 @@ class ChatViewModel : ViewModel() {
LogManager.addLog("INFO", "VM", "CALL_$tag id=0x%016x reason=$reason".format(callId))
if (st == null || st.callId != callId) return
callAudio.stop()
CallRingback.stop()
IncomingCallManager.stopRinging()
callIncomingAtMs = 0L
_muted.value = false
@@ -691,6 +694,7 @@ class ChatViewModel : ViewModel() {
_callState.value = CallState(callId = callId, channelId = channelId, peerNodeId = peerNodeId,
peerName = name, phase = CallPhase.OUTGOING)
NativeLib.callStart(channelId, peerNodeId, callId)
CallRingback.startConnecting()
LogManager.addLog("INFO", "VM", "startCall ch=$channelId peer=0x%016x id=0x%016x".format(peerNodeId, callId))
}
@@ -727,6 +731,7 @@ class ChatViewModel : ViewModel() {
fun dismissCall() {
callAudio.stop()
CallRingback.stop()
callIncomingAtMs = 0L
_callState.value = null
}