From a59ebcd6dbbe47c21795f084b1093222c3e615a8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 14 Oct 2026 22:45:34 +0200 Subject: [PATCH] Own Android call video in the foreground call service --- .../app/src/main/AndroidManifest.xml | 2 +- .../java/com/utun/chat/data/CallController.kt | 10 ++++++++++ .../main/java/com/utun/chat/data/CallService.kt | 16 ++++++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/tools/chatgui-android/app/src/main/AndroidManifest.xml b/tools/chatgui-android/app/src/main/AndroidManifest.xml index c04eadf9..def0d7da 100644 --- a/tools/chatgui-android/app/src/main/AndroidManifest.xml +++ b/tools/chatgui-android/app/src/main/AndroidManifest.xml @@ -82,7 +82,7 @@ + android:foregroundServiceType="phoneCall|microphone|camera" /> diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt index 8d2cec19..121dfec6 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt @@ -24,6 +24,14 @@ object CallController : TelecomCallManager.Listener { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val _state = MutableStateFlow(null) val state: StateFlow = _state + private val _video = MutableStateFlow(null) + val video: StateFlow = _video + fun setVideo(settings: CallVideoSettings) { + checkMain() + val owner = service ?: return + if (_state.value?.phase != CallPhase.ACTIVE) return + if (owner.setVideoCapture(settings.enabled)) owner.video?.select(settings) + } private val _muted = MutableStateFlow(false) val muted: StateFlow = _muted private val _route = MutableStateFlow(AudioRoute.SPEAKER) @@ -202,6 +210,7 @@ object CallController : TelecomCallManager.Listener { acceptSent = false answerWhenVisible = 0L owner?.shutdown() + _video.value = null proximity.stop() TelecomCallManager.setDisconnected(id, reason) TelecomCallManager.removeConnection(id) @@ -327,6 +336,7 @@ object CallController : TelecomCallManager.Listener { return } accepting = false + _video.value = owner.video TelecomCallManager.setActive(id) _state.value = st.copy(phase = CallPhase.ACTIVE, durationSeconds = 0, startedAtElapsedRealtime = SystemClock.elapsedRealtime()) owner.audio.router.onChanged = { route, headset -> routeChanged(route, headset) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallService.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallService.kt index fb31adee..d7cf377b 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallService.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallService.kt @@ -43,6 +43,9 @@ class CallService : Service() { } val audio = CallAudioEngine() + var video: CallVideoEngine? = null + private set + private var cameraForeground = false private var callId = 0L private var wakeLock: PowerManager.WakeLock? = null private var powerEventsRegistered = false @@ -137,7 +140,8 @@ class CallService : Service() { val micType = if (microphone && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE else 0 val types = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or micType + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or micType or + (if (cameraForeground && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA else 0) } else 0 ServiceCompat.startForeground(this, NOTIFICATION_ID, notification(), types) LogManager.addLog("INFO", "CallService", "foreground notification id=$callId phase=${CallController.state.value?.phase}") @@ -149,7 +153,14 @@ class CallService : Service() { return false } audio.router.setPreferredRoute(ChatApplication.instance.configProvider.getCallAudioRoute()) - return audio.start(id) + if (!audio.start(id)) return false + video = CallVideoEngine(this, id) + return true + } + + fun setVideoCapture(enabled: Boolean): Boolean { + return try { cameraForeground = enabled; promote(true); true } + catch (e: Exception) { cameraForeground = false; LogManager.addLog("ERROR", "call", "camera foreground failed: ${e.message}"); false } } fun updateNotification() { @@ -202,6 +213,7 @@ class CallService : Service() { private fun releaseResources() { val id = callId + video?.stop(); video = null; cameraForeground = false if (powerEventsRegistered) { try { unregisterReceiver(powerEvents) } catch (e: Exception) { LogManager.addLog("WARN", "CallService", "power receiver cleanup failed id=$id: ${e.message}") }