diff --git a/src/radio/radio.c b/src/radio/radio.c
index 15a18c95..603b276e 100644
--- a/src/radio/radio.c
+++ b/src/radio/radio.c
@@ -292,6 +292,13 @@ static void radio_recv(struct TOPO_GROUP* group, struct ETCP_CONN* from_conn,
return;
}
+ /* начало передачи (первый кадр burst): видно и на транзитных узлах (коло), не только у подписчиков */
+ if (seq == 0) {
+ DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: recv burst src=%016llx stream=%u fin=%u len=%d sub=%d",
+ RADIO_ID, (unsigned long long)src, stream, (flags & RADIO_FLAG_FIN) ? 1 : 0,
+ opus_len, group->radio_active ? 1 : 0);
+ }
+
/* доставка локально, если мы подписаны */
if (group->radio_active && ctx->group) {
struct radio_instance* ri = radio_inst(group->instance);
diff --git a/tools/chatgui-android/app/src/main/AndroidManifest.xml b/tools/chatgui-android/app/src/main/AndroidManifest.xml
index 42160bbc..bb0ebed6 100644
--- a/tools/chatgui-android/app/src/main/AndroidManifest.xml
+++ b/tools/chatgui-android/app/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
+
@@ -51,7 +52,7 @@
+ android:foregroundServiceType="microphone|specialUse">
diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
index 7be8b75c..bca9334e 100644
--- a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
+++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
@@ -49,6 +49,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
requestNotificationPermission()
+ requestFgsMicPermission()
handleIncomingCallIntent(intent)
/* Edge-to-edge with dark status/navigation bar icons (light app theme) */
@@ -398,6 +399,14 @@ class MainActivity : ComponentActivity() {
}
}
+ private fun requestFgsMicPermission() {
+ if (Build.VERSION.SDK_INT >= 34 &&
+ ContextCompat.checkSelfPermission(this, Manifest.permission.FOREGROUND_SERVICE_MICROPHONE) !=
+ PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(arrayOf(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE), 2)
+ }
+ }
+
override fun onStop() {
super.onStop()
if (NativeLib.isReady()) NativeLib.setActive(false)
diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt
index bedf2560..f99f9020 100644
--- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt
+++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt
@@ -45,6 +45,7 @@ class RadioAudioEngine {
@Volatile private var active = false
@Volatile private var groupId = 0L
@Volatile private var talking = false
+ @Volatile private var micDead = false
private val _talkingFlow = MutableStateFlow(false)
val talkingFlow: StateFlow = _talkingFlow
@@ -115,6 +116,7 @@ class RadioAudioEngine {
}
active = true
+ micDead = false
audioRecord?.startRecording()
audioTrack?.play()
@@ -153,6 +155,9 @@ class RadioAudioEngine {
/** PTT зажата — начинаем передачу. */
fun talkBegin() {
if (!active || talking) return
+ if (micDead) {
+ LogManager.addLog("WARN", "RadioAudio", "talkBegin but mic is dead (background capture stopped)")
+ }
talking = true
_talkingFlow.value = true
NativeLib.radioTalkBegin(groupId)
@@ -168,12 +173,26 @@ class RadioAudioEngine {
private fun captureLoop() {
val buf = ShortArray(FRAME_SAMPLES)
- while (active && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
+ while (active) {
+ val rec = audioRecord ?: break
+ if (rec.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
+ if (active) {
+ micDead = true
+ LogManager.addLog("WARN", "RadioAudio", "capture loop: recordingState=${rec.recordingState}, mic stopped")
+ }
+ break
+ }
var read = 0
while (read < FRAME_SAMPLES) {
if (!active) return
- val n = audioRecord?.read(buf, read, FRAME_SAMPLES - read) ?: return
- if (n <= 0) return
+ val n = rec.read(buf, read, FRAME_SAMPLES - read)
+ if (n <= 0) {
+ if (active) {
+ micDead = true
+ LogManager.addLog("WARN", "RadioAudio", "capture loop: read()=$n, mic lost")
+ }
+ return
+ }
read += n
}
if (talking && !NativeLib.radioAudioFeed(groupId, buf)) {
diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt
index c0f31193..910b5590 100644
--- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt
+++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt
@@ -1,10 +1,13 @@
package com.utun.chat.data
+import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
import android.graphics.PixelFormat
import android.graphics.drawable.GradientDrawable
import android.os.Build
@@ -18,6 +21,8 @@ import android.view.ViewConfiguration
import android.view.WindowManager
import android.widget.TextView
import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
+import androidx.core.content.ContextCompat
import kotlin.math.abs
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -44,6 +49,12 @@ class RadioOverlayService : Service() {
Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(ctx)
fun start(ctx: Context) {
+ if (Build.VERSION.SDK_INT >= 34 &&
+ ContextCompat.checkSelfPermission(ctx, Manifest.permission.FOREGROUND_SERVICE_MICROPHONE) !=
+ PackageManager.PERMISSION_GRANTED) {
+ LogManager.addLog("ERROR", "RadioOverlay", "FOREGROUND_SERVICE_MICROPHONE not granted — background mic will be muted")
+ return
+ }
val i = Intent(ctx, RadioOverlayService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
ctx.startForegroundService(i)
@@ -66,7 +77,12 @@ class RadioOverlayService : Service() {
override fun onCreate() {
super.onCreate()
createChannel()
- startForeground(NOTIF_ID, buildNotification())
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ ServiceCompat.startForeground(this, NOTIF_ID, buildNotification(),
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE)
+ } else {
+ startForeground(NOTIF_ID, buildNotification())
+ }
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
if (!canDrawOverlays(this)) {
diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt
index 4210eacf..80a416ec 100644
--- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt
+++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt
@@ -46,7 +46,7 @@ private val CATEGORIES = listOf(
"config", "tun", "routing", "timers", "bgp", "chat",
"socket", "control", "dump", "traffic", "debug", "general",
"nat", "keepalive", "media", "etcp_route", "etcp_dump",
- "chat_sync", "member_sync", "proxy"
+ "chat_sync", "member_sync", "proxy", "radio"
)
private val PROTOCOLS = listOf("tcp", "udp")