diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt index 45d795b7..57bed268 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt @@ -543,7 +543,7 @@ class ChatRepository(private val reportFailure: (String) -> Unit = { LogManager. /* ── DM (личные сообщения) ── */ - /** ID принятого запроса, привязанный к instance; null означает отказ очереди, событие ещё не подтверждает commit. */ + /** ID принятого запроса, привязанный к instance; null — отказ. Возврат ещё не подтверждает commit: нужен DM_CONV_UPDATED. */ fun dmStart(nodeId: Long, sourceChId: String, peerName: String): String? = NativeLib.dmStart(nodeId, sourceChId, peerName) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index f070716a..4352f05c 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -10,6 +10,10 @@ interface ChatEventCallback { fun onEvent(type: Int, data: ByteArray?) } +/** JNI API. start/stop/restart/resetDatabase сериализованы lifecycle mutex и вызываются из coroutine. + * Stop сначала ждёт frontend audio/video owners, затем join worker на IO; isReady отражает native ready. + * Очередь JNI подтверждает приём команды, результат операции приходит отдельным CHAT_EVT_*. + * JSON snapshots читаются в worker синхронно: тяжёлые getters вызывать на IO. */ object NativeLib { @Volatile private var coreStopping = false @Volatile private var eventCallback: ChatEventCallback? = null @@ -289,9 +293,14 @@ object NativeLib { /* Live media используют отдельную ограниченную очередь, минуя чатовый event bus. */ fun streamOpen(id: Long, group: Long, source: Long = 0, width: Int = 0, height: Int = 0, - rotation: Int = 0, audio: Boolean = true, epoch: Long = 0, videoCodec: Int = 1): Boolean = - nativeStreamOpen(id, group, source, width, height, rotation, audio, epoch, videoCodec) - fun streamWatch(group: Long): Boolean = nativeStreamWatch(group) + rotation: Int = 0, audio: Boolean = true, epoch: Long = 0, videoCodec: Int = 1): Boolean { + if (coreStopping) { LogManager.addLog("WARN", "stream", "Open rejected during core teardown id=$id group=$group"); return false } + return nativeStreamOpen(id, group, source, width, height, rotation, audio, epoch, videoCodec) + } + fun streamWatch(group: Long): Boolean { + if (coreStopping) { LogManager.addLog("WARN", "stream", "Watch rejected during core teardown group=$group"); return false } + return nativeStreamWatch(group) + } fun streamFeed(id: Long, video: Boolean, pts: Long, key: Boolean, data: ByteArray, revision: Long = 0, width: Int = 0, height: Int = 0, rotation: Int = 0, audio: Boolean = true, videoCodec: Int = 1): Int = nativeStreamFeed(id, video, pts, key, data, revision, width, height, rotation, audio, videoCodec) @@ -308,7 +317,10 @@ object NativeLib { external fun streamSignalNotify(handle: Long) external fun streamSignalWait(handle: Long, timeoutMs: Int): Int external fun streamSignalClose(handle: Long) - fun streamPlaybackCreate(id: Long, pts: Long, audio: Boolean): Long = nativeStreamPlaybackCreate(id, pts, audio) + fun streamPlaybackCreate(id: Long, pts: Long, audio: Boolean): Long { + if (coreStopping) { LogManager.addLog("WARN", "stream", "Playback rejected during core teardown id=$id"); return 0 } + return nativeStreamPlaybackCreate(id, pts, audio) + } fun streamPlaybackFeed(handle: Long, pts: Long, packet: ByteArray): Int = nativeStreamPlaybackFeed(handle, pts, packet) fun streamPlaybackPrepare(handle: Long): Int = nativeStreamPlaybackPrepare(handle) fun streamPlaybackBuffered(handle: Long): Int = nativeStreamPlaybackBuffered(handle) @@ -390,7 +402,10 @@ object NativeLib { private external fun nativeRadioCall(id: Long, group: Long, peer: Long, action: Int): Boolean /* ── P2P аудио-звонок (медиа) ── */ - fun callAudioStart(callId: Long): Boolean = !coreStopping && nativeCallAudioStart(callId) + fun callAudioStart(callId: Long): Boolean { + if (coreStopping) { LogManager.addLog("WARN", "Call", "Audio start rejected during core teardown id=$callId"); return false } + return nativeCallAudioStart(callId) + } /** Настоящий raw PCM и mute кадра: зануление выполняется в core после AEC/AGC. */ fun callAudioFeed(callId: Long, samples: ShortArray, muted: Boolean, capturePtsUs: Long): Boolean = nativeCallAudioFeed(callId, samples, muted, capturePtsUs) @@ -410,11 +425,17 @@ object NativeLib { /* ── Рация (PTT walkie-talkie) ── */ fun radioSetActive(groupId: Long, on: Boolean) { nativeRadioSetActive(groupId, on) } fun radioSubscribers(groupId: Long): Int = nativeRadioSubscribers(groupId) - fun radioAudioStart(groupId: Long, channels: Int): Boolean = !coreStopping && nativeRadioAudioStart(groupId, channels) + fun radioAudioStart(groupId: Long, channels: Int): Boolean { + if (coreStopping) { LogManager.addLog("WARN", "Radio", "Audio start rejected during core teardown group=$groupId"); return false } + return nativeRadioAudioStart(groupId, channels) + } fun radioAudioFeed(groupId: Long, samples: ShortArray): Boolean = nativeRadioAudioFeed(groupId, samples) fun radioAudioPull(groupId: Long, out: ShortArray, playedFrames: Long): Int = nativeRadioAudioPull(groupId, out, playedFrames) fun radioAudioStop() { nativeRadioAudioStop() } - fun radioCaptureStart(groupId: Long, channels: Int, mode: Int): Boolean = !coreStopping && nativeRadioCaptureStart(groupId, channels, mode) + fun radioCaptureStart(groupId: Long, channels: Int, mode: Int): Boolean { + if (coreStopping) { LogManager.addLog("WARN", "Radio", "Capture rejected during core teardown group=$groupId mode=$mode"); return false } + return nativeRadioCaptureStart(groupId, channels, mode) + } /** true — копия снимка поставлена живому worker; проверка и применение ещё не подтверждены. * ConfigProvider.onRadioSettingsApplied вернёт тот же requestId и результат атомарной проверки/применения. * false — запрос отклонён (включая отсутствие callback). Stop/restart может отменить ожидающий снимок без ACK;