radio/android: PTT FGS microphone permission + детект потери микрофона
- FOREGROUND_SERVICE_MICROPHONE запрос + foregroundServiceType microphone|specialUse (Android 14+) - RadioOverlayService.start: не стартовать без mic-разрешения, ServiceCompat.startForeground с TYPE_MICROPHONE - RadioAudioEngine: micDead-флаг и лог при обрыве captureLoop/read - radio.c: лог первого кадра burst на транзитных узлах - SettingsScreen: категория radio в списке debug-категорий
This commit is contained in:
1 parent
688785fc6e
commit
6b92964794
6 files changed
+58
-6
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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<Boolean> = _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)) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in new issue
Block a user