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