Browse Source

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

router-recv-conn-uaf
evgeny 2 weeks ago
parent
commit
b483335c06
  1. 50
      src/routing_layer/topo_group.c
  2. 1
      src/routing_layer/topo_group.h
  3. 20
      src/routing_layer/topo_group_connect.c
  4. 20
      src/routing_layer/topo_group_invite.c
  5. 6
      src/routing_layer/topo_recovery.c
  6. 17
      src/transport_layer/node_conn_direct.c
  7. 17
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioRouter.kt
  8. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt
  9. 12
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt
  10. 14
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt
  11. 3
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  12. 1
      tools/chatgui/resources/chatgui.qrc
  13. 22
      tools/chatgui/src/mainwindow.cpp
  14. 34
      tools/chatgui/src/sound_manager.cpp
  15. 3
      tools/chatgui/src/sound_manager.h
  16. 9
      tools/chatgui/src/soundsettingspage.cpp
  17. 2
      tools/chatgui/src/soundsettingspage.h

50
src/routing_layer/topo_group.c

@ -23,6 +23,7 @@
#include "route_connectivity.h"
#include "control_server.h"
#include "conn_mgr.h"
#include "../transport_layer/node_conn_direct.h"
#include "topo_group_connect.h"
#include "topo_recovery.h"
#include "broadcast.h"
@ -324,7 +325,11 @@ static void topo_group_destroy(struct TOPO_GROUP* group) {
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[GRP_DESTROY] 2b broadcast_destroy done");
struct ll_entry* e;
while ((e = queue_data_get(group->senders_list)) != NULL) queue_entry_free(e);
while ((e = queue_data_get(group->senders_list)) != NULL) {
struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e;
if (item->handle) { node_conn_direct_close(item->handle); item->handle = NULL; }
queue_entry_free(e);
}
queue_free(group->senders_list);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[GRP_DESTROY] 3 senders_list done");
@ -597,6 +602,24 @@ void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
}
topo_group_add_to_senders(group, conn);
/* BGP — владелец conn: держим NCD-handle (если ещё нет), чтобы соединение
* не рвалось другими владельцами (conn_mgr) при их закрытии. cb=NULL —
* события BGP уже получает через conn-status callback. */
{ struct ll_entry* se = group->senders_list->head;
while (se) {
struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)se->data;
if (item->conn == conn) {
if (item->handle == NULL) {
node_conn_direct_open(conn->instance, conn->peer_node_id, NULL, NULL, &item->handle, NULL);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "topo_group_new_conn: BGP handle acquired node=0x%016llx h=%p",
(unsigned long long)conn->peer_node_id, (void*)item->handle);
}
break;
}
se = se->next;
}
}
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "topo_group_new_conn: peer=%016llx group=%016llx type=%d ch=%s", (unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id, group->group_type, group->channel_id);
topo_group_send_table_request(group, conn);
@ -658,8 +681,24 @@ void topo_group_remove_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
node_entry = next;
}
/* освобождаем владение BGP: сначала убираем item из senders_list (чтобы
ре-ентрантный remove_conn от conn DOWN не нашёл его), затем закрываем handle. */
e = group->senders_list->head;
while (e) { struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data; if (item->conn == conn) { queue_remove_data(group->senders_list, e); queue_entry_free(e); break; } e = e->next; }
while (e) {
struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data;
if (item->conn == conn) {
struct NODE_CONN_DIRECT* h = item->handle;
item->handle = NULL;
queue_remove_data(group->senders_list, e);
queue_entry_free(e);
if (h) {
node_conn_direct_close(h);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "topo_group_remove_conn: BGP handle released node=0x%016llx", (unsigned long long)conn->peer_node_id);
}
break;
}
e = e->next;
}
if (cascaded > 0) topo_recovery_start(group);
@ -1019,7 +1058,12 @@ static void topo_group_add_to_senders(struct TOPO_GROUP* group, struct ETCP_CONN
while (e) { if (((struct TOPO_GROUP_CONN_ITEM*)e->data)->conn == conn) { already = true; break; } e = e->next; }
if (!already) {
struct ll_entry* item_entry = queue_entry_new(sizeof(struct TOPO_GROUP_CONN_ITEM));
if (item_entry) { ((struct TOPO_GROUP_CONN_ITEM*)item_entry->data)->conn = conn; queue_data_put(group->senders_list, item_entry); }
if (item_entry) {
struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)item_entry->data;
item->conn = conn;
item->handle = NULL;
queue_data_put(group->senders_list, item_entry);
}
}
}

1
src/routing_layer/topo_group.h

@ -164,6 +164,7 @@ struct TOPOMSG_ERR_GROUP_MISMATCH {
struct TOPO_GROUP_CONN_ITEM {
struct ll_entry ll;
struct ETCP_CONN* conn;
struct NODE_CONN_DIRECT* handle; // владение BGP этим conn (NCD-handle)
};
struct ETCP_LINK;

20
src/routing_layer/topo_group_connect.c

@ -275,6 +275,18 @@ void topo_group_connect_on_down(struct TOPO_GROUP* group, struct ETCP_CONN* conn
* В Phase 2/3 — успех завершает цикл; провал закрывает handle (чтобы не оставить
* залипшее состояние) и ставит паузу перед следующей попыткой.
*/
/* Передача владения: после topo_group_new_conn (который взял свой NCD-handle)
* закрываем connect-фазу handle, чтобы владельцем conn остался один BGP-хэндл
* (в senders_list), без двойного счёта. */
static void tgc_release_handle(struct TOPO_GROUP_CONNECT* gc, struct NODE_CONN_DIRECT* h) {
if (!gc || !h) return;
for (int i = 0; i < gc->handle_count; i++) {
if (gc->handles[i] == h) { gc->handles[i] = NULL; node_conn_direct_close(h); return; }
}
if (gc->cur_handle == h) { gc->cur_handle = NULL; node_conn_direct_close(h); return; }
node_conn_direct_close(h);
}
static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) {
struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg;
struct ETCP_CONN* conn = node_conn_direct_get_conn(h);
@ -283,10 +295,9 @@ static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void*
int ok = (event == NCD_EVENT_UP);
if (ok) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id);
if (nq) nq->handle = h;
/* запуск BGP: узел соединён с группой */
/* запуск BGP: узел соединён с группой (topo_group_new_conn берёт владение NCD-handle) */
if (conn) topo_group_new_conn(gc->group, conn);
tgc_release_handle(gc, h);
} else if (event == NCD_EVENT_TIMEOUT) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id);
if (nq) nq->handle = NULL;
@ -540,10 +551,9 @@ static void tgc_once_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg
struct ETCP_CONN* conn = node_conn_direct_get_conn(h);
if (ev == NCD_EVENT_UP && conn) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(c->group, c->node_id);
if (nq) nq->handle = h;
node_conn_direct_set_callback(h, NULL, NULL);
topo_group_new_conn(c->group, conn);
node_conn_direct_close(h); /* владение перешло к topo_group_new_conn */
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: once-connect UP node=0x%016llx ch=%s",
TGC_ID, (unsigned long long)c->node_id, c->group->channel_id);
} else if (ev == NCD_EVENT_TIMEOUT) {

20
src/routing_layer/topo_group_invite.c

@ -343,15 +343,16 @@ static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, si
topo_group_new_conn(bgp_grp, conn);
}
/* ── успех: переводим ncd-handle во владение узла, доставляем JOIN ── */
/* ── успех: владение conn перешло к topo_group_new_conn (item->handle),
закрываем connect-фазу handle и доставляем JOIN ── */
{
uint64_t node_id = inv->node_id, group_id = inv->group_id;
tgi_cb_t cb = inv->cb; void* cb_arg = inv->cb_arg;
if (inv->ncd_handle) node_conn_direct_set_callback(inv->ncd_handle, NULL, NULL);
struct TOPO_GROUP* g = topo_groups_find(inv->inst->topo_groups, group_id);
struct TOPO_GROUP_NODE* nq = g ? topo_node_find_by_id(g, node_id) : NULL;
if (nq) nq->handle = inv->ncd_handle;
inv->ncd_handle = NULL; /* не закрываем — соединение переходит узлу */
if (inv->ncd_handle) {
node_conn_direct_set_callback(inv->ncd_handle, NULL, NULL);
node_conn_direct_close(inv->ncd_handle);
inv->ncd_handle = NULL;
}
inv->temp_nq = NULL; /* не удаляем узел из группы */
tgi_cancel(inv);
if (cb) cb(node_id, group_id, TGI_EVENT_JOIN, cb_arg);
@ -511,10 +512,11 @@ struct tgi_conn_ctx { struct UTUN_INSTANCE* inst; uint64_t node_id, group_id; };
static void tgi_to_channel_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg) {
struct tgi_conn_ctx* c = (struct tgi_conn_ctx*)arg;
if (ev == NCD_EVENT_UP) {
struct ETCP_CONN* conn = node_conn_direct_get_conn(h);
struct TOPO_GROUP* g = topo_groups_find(c->inst->topo_groups, c->group_id);
struct TOPO_GROUP_NODE* nq = g ? topo_node_find_by_id(g, c->node_id) : NULL;
if (nq) nq->handle = h;
node_conn_direct_set_callback(h, NULL, NULL);
/* владение переходит к topo_group_new_conn (item->handle), connect-фазу закрываем */
if (g && conn) topo_group_new_conn(g, conn);
node_conn_direct_close(h);
u_free(c);
} else if (ev == NCD_EVENT_TIMEOUT) {
node_conn_direct_force_close(h);

6
src/routing_layer/topo_recovery.c

@ -68,9 +68,9 @@ static void topo_recovery_callback(struct NODE_CONN_DIRECT* h, enum ncd_event ev
ctx->current_node_id = 0;
ctx->current_handle = NULL;
if (event == NCD_EVENT_UP) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(ctx->group, node_id);
if (nq) nq->handle = h;
node_conn_direct_set_callback(h, NULL, NULL);
/* владение переходит к topo_group_new_conn (item->handle), connect-фазу закрываем */
if (conn) topo_group_new_conn(ctx->group, conn);
node_conn_direct_close(h);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx reconnected to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id);
} else {
node_conn_direct_close(h);

17
src/transport_layer/node_conn_direct.c

@ -869,16 +869,21 @@ void node_conn_direct_close(struct NODE_CONN_DIRECT* h) {
/* Удаляем handle из списка */
struct NODE_CONN_DIRECT** pp = &entry->handles;
while (*pp) { if (*pp == h) { *pp = h->next; break; } pp = &(*pp)->next; }
int found = 0;
while (*pp) { if (*pp == h) { *pp = h->next; found = 1; break; } pp = &(*pp)->next; }
if (!found) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] close: h=%p NOT in handles list node=0x%016llx (double-close?)",
(void*)h, (unsigned long long)node_id);
}
entry->handle_count--;
if (entry->handle_count < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: handle_count=%d < 0 node=0x%016llx",
entry->handle_count, (unsigned long long)node_id);
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: handle_count=%d < 0 h=%p node=0x%016llx",
entry->handle_count, (void*)h, (unsigned long long)node_id);
h->entry = NULL; u_free(h); return;
}
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] close node=0x%016llx remaining=%d", (unsigned long long)node_id, entry->handle_count);
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] close h=%p node=0x%016llx remaining=%d", (void*)h, (unsigned long long)node_id, entry->handle_count);
if (entry->handle_count == 0) {
if (entry->handles) {
@ -959,8 +964,8 @@ void node_conn_direct_force_close(struct NODE_CONN_DIRECT* h) {
entry->handle_count--;
if (entry->handle_count < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: force_close handle_count=%d < 0 node=0x%016llx",
entry->handle_count, (unsigned long long)node_id);
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: force_close handle_count=%d < 0 h=%p node=0x%016llx",
entry->handle_count, (void*)h, (unsigned long long)node_id);
h->entry = NULL; u_free(h); return;
}

17
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioRouter.kt

@ -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)
}

2
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt

@ -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
}

12
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt

@ -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> {

14
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt

@ -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
)
}

3
tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

@ -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")
}

1
tools/chatgui/resources/chatgui.qrc

@ -10,6 +10,7 @@
<file>icons/mic.png</file>
<file>icons/send.png</file>
<file>sounds/msg_incoming.mp3</file>
<file>sounds/call_incoming.mp3</file>
<file>bg.jpg</file>
</qresource>
</RCC>

22
tools/chatgui/src/mainwindow.cpp

@ -292,7 +292,7 @@ void MainWindow::setupSoundFromConfig() {
SoundManager* sm = SoundManager::instance();
if (!sm->init()) return;
QString soundDir, msgInFile, msgOutFile, connectFile, disconnectFile;
QString soundDir, msgInFile, msgOutFile, connectFile, disconnectFile, callInFile;
QFile cfgFile(m_cfgPath);
if (cfgFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
bool inGui = false;
@ -307,6 +307,7 @@ void MainWindow::setupSoundFromConfig() {
else if (line.startsWith("sound_msg_outgoing=")) msgOutFile = line.mid(19).trimmed();
else if (line.startsWith("sound_connect=")) connectFile = line.mid(14).trimmed();
else if (line.startsWith("sound_disconnect=")) disconnectFile = line.mid(17).trimmed();
else if (line.startsWith("sound_call_incoming=")) callInFile = line.mid(20).trimmed();
}
cfgFile.close();
}
@ -321,16 +322,21 @@ void MainWindow::setupSoundFromConfig() {
else
sm->loadSound("msg_incoming", resolvePath(msgInFile));
if (callInFile.isEmpty())
sm->loadSound("call_incoming", ":/sounds/call_incoming.mp3");
else
sm->loadSound("call_incoming", resolvePath(callInFile));
if (!msgOutFile.isEmpty()) sm->loadSound("msg_outgoing", resolvePath(msgOutFile));
if (!connectFile.isEmpty()) sm->loadSound("connect", resolvePath(connectFile));
if (!disconnectFile.isEmpty()) sm->loadSound("disconnect", resolvePath(disconnectFile));
if (m_db && m_db->isOpen()) {
sm->setEnabled(m_db->getUiStateInt("sound_enabled", 1));
sm->setVolume(m_db->getUiStateInt("sound_volume", 80) / 100.0f);
QString se[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect"};
for (int i = 0; i < 4; i++) {
QString se[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect", "call_incoming"};
for (int i = 0; i < 5; i++) {
const QString& ek = se[i];
sm->setEvent(ek, m_db->getUiStateInt(QStringLiteral("sound_%1_on").arg(ek), i == 0 ? 1 : 0),
sm->setEvent(ek, m_db->getUiStateInt(QStringLiteral("sound_%1_on").arg(ek), (i == 0 || i == 4) ? 1 : 0),
m_db->getUiStateInt(QStringLiteral("sound_%1_vol").arg(ek), 80) / 100.0f);
}
}
@ -528,6 +534,12 @@ void MainWindow::onCallIncoming(const QString& chId, quint64 caller, quint64 cal
QString name = m_db->getDisplayName(caller);
if (name.isEmpty()) name = QStringLiteral("0x%1").arg(caller, 16, 16, QChar('0'));
SoundManager::instance()->playLoop("call_incoming");
if (!isVisible() || isMinimized()) {
m_trayIcon->showMessage("vibechat", QStringLiteral("Incoming call from %1").arg(name),
QSystemTrayIcon::Information, 5000);
}
CallWindow* w = new CallWindow(this);
w->setCallId(callId);
w->setPeerName(name);
@ -545,6 +557,7 @@ void MainWindow::onCallRinging(quint64 callId) {
}
void MainWindow::onCallAccepted(quint64 callId) {
SoundManager::instance()->stopLoop();
CallWindow* w = m_activeCalls.value(callId, nullptr);
if (w) w->setState(CallWindow::Active);
if (m_db)
@ -561,6 +574,7 @@ void MainWindow::onCallEnded(quint64 callId, uint8_t reason) {
}
void MainWindow::closeCallWindow(quint64 callId) {
SoundManager::instance()->stopLoop();
CallWindow* w = m_activeCalls.take(callId);
if (!w) return;
w->disconnect(this);

34
tools/chatgui/src/sound_manager.cpp

@ -56,6 +56,7 @@ void SoundManager::shutdown() {
if (m_engine) { ma_engine_uninit(m_engine); delete m_engine; m_engine = nullptr; }
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: after ma_engine_uninit");
m_initialized = false;
m_loopName.clear();
}
void SoundManager::setEnabled(bool enabled) { m_enabled = enabled; }
@ -151,6 +152,39 @@ void SoundManager::play(const QString& name) {
ma_sound_start(s);
}
void SoundManager::playLoop(const QString& name) {
if (!m_enabled) return;
if (!m_initialized) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "SoundManager::playLoop: not initialized"); return; }
EventSettings es = m_eventSettings.value(name);
if (!es.enabled) return;
auto it = m_sounds.find(name);
if (it == m_sounds.end()) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "SoundManager::playLoop: sound '%s' not loaded", qPrintable(name));
return;
}
ma_sound* s = it->sound;
ma_sound_stop(s);
ma_sound_seek_to_pcm_frame(s, 0);
ma_sound_set_volume(s, m_volume * es.volume);
ma_sound_set_looping(s, MA_TRUE);
ma_sound_start(s);
m_loopName = name;
}
void SoundManager::stopLoop() {
if (m_loopName.isEmpty()) return;
auto it = m_sounds.find(m_loopName);
m_loopName.clear();
if (it == m_sounds.end()) return;
ma_sound* s = it->sound;
ma_sound_stop(s);
ma_sound_set_looping(s, MA_FALSE);
ma_sound_seek_to_pcm_frame(s, 0);
}
ma_context* SoundManager::context() {
return m_context;
}

3
tools/chatgui/src/sound_manager.h

@ -35,6 +35,8 @@ public:
bool loadSound(const QString& name, const QString& filePath);
void play(const QString& name);
void playLoop(const QString& name);
void stopLoop();
ma_context* context();
@ -76,4 +78,5 @@ private:
float m_volume = 0.8f;
QHash<QString, EventSettings> m_eventSettings;
QHash<QString, LoadedSound> m_sounds;
QString m_loopName;
};

9
tools/chatgui/src/soundsettingspage.cpp

@ -59,8 +59,9 @@ SoundSettingsPage::SoundSettingsPage(DbManager* db, QWidget* parent)
m_events[1] = {"msg_outgoing", "Outgoing message", nullptr, nullptr, nullptr};
m_events[2] = {"connect", "Peer connected", nullptr, nullptr, nullptr};
m_events[3] = {"disconnect", "Peer disconnected", nullptr, nullptr, nullptr};
m_events[4] = {"call_incoming", "Incoming call", nullptr, nullptr, nullptr};
for (int i = 0; i < 4; i++) {
for (int i = 0; i < 5; i++) {
auto& e = m_events[i];
auto* row = new QHBoxLayout();
row->setSpacing(8);
@ -118,11 +119,11 @@ void SoundSettingsPage::loadFromDb() {
m_volumeLabel->setEnabled(enabled);
m_volumeLabel->setText(QString::number(vol) + "%");
for (int i = 0; i < 4; i++) {
for (int i = 0; i < 5; i++) {
auto& e = m_events[i];
QString onKey = QStringLiteral("sound_%1_on").arg(e.key);
QString volKey = QStringLiteral("sound_%1_vol").arg(e.key);
bool on = m_db->getUiStateInt(onKey, i == 0 ? 1 : 0); // only msg_incoming on by default
bool on = m_db->getUiStateInt(onKey, (i == 0 || i == 4) ? 1 : 0); // msg_incoming и call_incoming on by default
int ev = m_db->getUiStateInt(volKey, 80);
e.check->setChecked(on);
e.slider->setValue(ev);
@ -138,7 +139,7 @@ void SoundSettingsPage::applyAndSave() {
sm->setEnabled(enabled);
sm->setVolume(masterVol);
for (int i = 0; i < 4; i++) {
for (int i = 0; i < 5; i++) {
auto& e = m_events[i];
bool on = e.check->isChecked();
float ev = e.slider->value() / 100.0f;

2
tools/chatgui/src/soundsettingspage.h

@ -28,5 +28,5 @@ private:
QCheckBox* m_enabledCheck;
QSlider* m_volumeSlider;
QLabel* m_volumeLabel;
EventRow m_events[4];
EventRow m_events[5];
};

Loading…
Cancel
Save