bgp/ncd: владение conn через NCD-handle в senders_list; call audio route persist + incoming call sound

This commit is contained in:
evgeny committed 2026-09-16 13:29:28 +03:00
1 parent b11e77e4c0
commit b483335c06
17 files changed
+185 -48

No files matched your search

@@ -35,6 +35,8 @@ class CallAudioRouter {
@Volatile var route = AudioRoute.SPEAKER
private set
@Volatile var preferredRoute = AudioRoute.SPEAKER
private set
@Volatile var headsetAvailable = false
private set
@Volatile var hasEarpiece = true
@@ -71,7 +73,7 @@ class CallAudioRouter {
bindBtHeadsetProfile()
hasEarpiece = findBuiltinEarpiece() != null
route = AudioRoute.SPEAKER
route = preferredRoute
applyRoute(route)
refresh()
LogManager.addLog("INFO", "CallAudioRouter", "started (hasEarpiece=$hasEarpiece)")
@@ -102,11 +104,18 @@ class CallAudioRouter {
}
if (route == r) return
route = r
preferredRoute = r
applyRoute(r)
LogManager.addLog("INFO", "CallAudioRouter", "route -> $r (headsetAvailable=$headsetAvailable)")
onChanged?.invoke(route, headsetAvailable)
}
/** Инжекция сохранённого источника аудио (из DataStore) перед стартом звонка. */
fun setPreferredRoute(r: AudioRoute) {
preferredRoute = r
LogManager.addLog("INFO", "CallAudioRouter", "preferredRoute -> $r")
}
/* ── применение маршрута ── */
private fun applyRoute(r: AudioRoute) {
try {
@@ -200,6 +209,12 @@ class CallAudioRouter {
applyRoute(route)
LogManager.addLog("INFO", "CallAudioRouter", "headset lost, fallback route -> EARPIECE")
}
/* гарнитура появилась, а пользователь ранее выбрал её — восстанавливаем */
if (now && !prev && preferredRoute == AudioRoute.HEADSET && route != AudioRoute.HEADSET) {
route = AudioRoute.HEADSET
applyRoute(route)
LogManager.addLog("INFO", "CallAudioRouter", "headset available, restore preferred route -> HEADSET")
}
if (now != prev || route == AudioRoute.HEADSET) {
onChanged?.invoke(route, now)
}
@@ -24,7 +24,7 @@ data class NodeStatus(
val bgpRtt: Int = 0,
val bgpNodes: Int = 0
) {
val online: Boolean get() = (directPresence && directUp) || (indirectPresence && indirectUp)
val online: Boolean get() = hasPresence
val hasPresence: Boolean get() = directPresence || indirectPresence || bgpPresence
}
@@ -52,6 +52,7 @@ class ConfigProvider(private val context: Context) {
private val serviceEnabledKey = booleanPreferencesKey("service_enabled")
private val soundEnabledKey = booleanPreferencesKey("sound_enabled")
private val mutedChannelsKey = stringSetPreferencesKey("muted_channels")
private val callAudioRouteKey = stringPreferencesKey("call_audio_route")
private fun keysFilePath(): String {
val dir = runBlocking { context.dataStore.data.first()[dbPath] } ?: context.filesDir.absolutePath
@@ -247,6 +248,17 @@ class ConfigProvider(private val context: Context) {
}
}
/* ── Audio route for calls (запоминание источника аудио) ── */
fun getCallAudioRoute(): AudioRoute = runBlocking {
val s = context.dataStore.data.first()[callAudioRouteKey]
AudioRoute.entries.firstOrNull { it.name == s } ?: AudioRoute.SPEAKER
}
suspend fun setCallAudioRoute(r: AudioRoute) {
context.dataStore.edit { prefs -> prefs[callAudioRouteKey] = r.name }
}
/* ── Server entries ── */
suspend fun detectInterfaces(saved: List<ServerEntry> = emptyList()): List<ServerEntry> {
@@ -141,17 +141,9 @@ private fun MemberListItem(
) {
val onlineColor = when {
member.connecting -> Color(0xFFFFEB3B) // yellow: connecting
member.directUp || member.indirectUp -> Color(0xFF4CAF50) // green: up
member.directPresence || member.indirectPresence || member.bgpPresence -> Color(0xFFFFA726) // orange: presence but down
member.online -> Color(0xFF4CAF50) // green: reachable (direct/indirect/relay)
else -> Color(0xFF9E9E9E) // gray: offline
}
val connLabel = when {
member.connecting -> "connecting"
member.directUp -> "direct"
member.indirectUp -> "indirect"
member.directPresence || member.indirectPresence || member.bgpPresence -> "offline"
else -> ""
}
val bgColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent
Surface(
@@ -213,14 +205,14 @@ private fun MemberListItem(
else if (member.connecting) parts.add("connecting…")
else if (member.directUp) parts.add("direct↑")
else if (member.indirectUp) parts.add("indirect↑")
else if (member.directPresence || member.indirectPresence || member.bgpPresence) parts.add("down")
else if (member.online) parts.add("relay")
if (member.rtt > 0) parts.add("${member.rtt}ms")
if (parts.isNotEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = parts.joinToString(" "),
fontSize = 11.sp,
color = if (member.isSelf || member.directUp || member.indirectUp)
color = if (member.isSelf || member.online)
Color(0xFF4CAF50) else Color.Gray
)
}
@@ -503,6 +503,8 @@ class ChatViewModel : ViewModel() {
val st = _callState.value
if (st != null && st.callId == callId) {
LogManager.addLog("INFO", "VM", "CALL_ACCEPTED id=0x%016x".format(callId))
callAudio.router.setPreferredRoute(
ChatApplication.instance.configProvider.getCallAudioRoute())
if (!callAudio.start(callId)) {
LogManager.addLog("ERROR", "VM", "call audio engine start failed")
NativeLib.callHangup(callId)
@@ -629,6 +631,7 @@ class ChatViewModel : ViewModel() {
fun setAudioRoute(r: AudioRoute) {
callAudio.router.setRoute(r)
viewModelScope.launch { ChatApplication.instance.configProvider.setCallAudioRoute(r) }
LogManager.addLog("INFO", "VM", "setAudioRoute -> $r")
}