Defer video speaker activation until the phone is away from the ear
This commit is contained in:
1 parent
1b3d9ff8ee
commit
f61e1c0a9f
2 files changed
+61
-3
No files matched your search
@@ -8,9 +8,13 @@ import com.utun.chat.MainActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
@@ -26,6 +30,10 @@ object CallController : TelecomCallManager.Listener {
|
||||
val state: StateFlow<CallState?> = _state
|
||||
private val _video = MutableStateFlow<CallVideoEngine?>(null)
|
||||
val video: StateFlow<CallVideoEngine?> = _video
|
||||
private val _videoActive = MutableStateFlow(false)
|
||||
val videoActive: StateFlow<Boolean> = _videoActive
|
||||
private var videoJob: Job? = null
|
||||
private var videoSpeakerPending = false
|
||||
fun setVideo(settings: CallVideoSettings) {
|
||||
checkMain()
|
||||
val owner = service ?: return
|
||||
@@ -41,7 +49,7 @@ object CallController : TelecomCallManager.Listener {
|
||||
private val _earpiece = MutableStateFlow(true)
|
||||
val earpiece: StateFlow<Boolean> = _earpiece
|
||||
private val proximity = ProximityManager(ChatApplication.instance)
|
||||
val proximityNear: StateFlow<Boolean> = proximity.near
|
||||
val proximityNear: StateFlow<Boolean?> = proximity.near
|
||||
|
||||
private var service: CallService? = null
|
||||
|
||||
@@ -60,6 +68,7 @@ object CallController : TelecomCallManager.Listener {
|
||||
if (initialized) return
|
||||
initialized = true
|
||||
TelecomCallManager.setListener(this)
|
||||
scope.launch { proximity.near.collect { applyVideoSpeakerIfReady() } }
|
||||
scope.launch {
|
||||
while (true) {
|
||||
delay(500)
|
||||
@@ -209,6 +218,7 @@ object CallController : TelecomCallManager.Listener {
|
||||
accepting = false
|
||||
acceptSent = false
|
||||
answerWhenVisible = 0L
|
||||
videoJob?.cancel(); videoJob = null; videoSpeakerPending = false; _videoActive.value = false
|
||||
owner?.shutdown()
|
||||
_video.value = null
|
||||
proximity.stop()
|
||||
@@ -256,7 +266,9 @@ object CallController : TelecomCallManager.Listener {
|
||||
|
||||
fun setRoute(route: AudioRoute) {
|
||||
checkMain()
|
||||
videoSpeakerPending = false // Явный выбор пользователя отменяет ожидающее автоматическое переключение.
|
||||
service?.audio?.router?.setRoute(route)
|
||||
syncProximity()
|
||||
_state.value?.let { TelecomCallManager.setRoute(it.callId, route) }
|
||||
scope.launch { ChatApplication.instance.configProvider.setCallAudioRoute(route) }
|
||||
}
|
||||
@@ -264,10 +276,30 @@ object CallController : TelecomCallManager.Listener {
|
||||
private fun routeChanged(route: AudioRoute, headset: Boolean) {
|
||||
_route.value = route
|
||||
_headset.value = headset
|
||||
if (route == AudioRoute.EARPIECE && _state.value?.phase == CallPhase.ACTIVE) proximity.start() else proximity.stop()
|
||||
if (route != AudioRoute.EARPIECE) videoSpeakerPending = false
|
||||
syncProximity()
|
||||
service?.audio?.syncAecForRoute(route)
|
||||
}
|
||||
|
||||
private fun syncProximity() {
|
||||
if (_state.value?.phase == CallPhase.ACTIVE && _route.value == AudioRoute.EARPIECE
|
||||
&& (!_videoActive.value || videoSpeakerPending)) proximity.start() else proximity.stop()
|
||||
}
|
||||
|
||||
private fun applyVideoSpeakerIfReady() {
|
||||
if (!videoSpeakerPending || !_videoActive.value || proximity.near.value != false || _route.value != AudioRoute.EARPIECE) return
|
||||
val st = _state.value ?: return
|
||||
val audio = service?.audio ?: return
|
||||
if (st.phase != CallPhase.ACTIVE) return
|
||||
videoSpeakerPending = false
|
||||
// Сохранённый маршрут аудиозвонков не меняем; автоследование за гарнитурой сохраняется.
|
||||
audio.router.fallbackRoute = AudioRoute.SPEAKER
|
||||
audio.router.setRoute(AudioRoute.SPEAKER, automatic = true)
|
||||
TelecomCallManager.setRoute(st.callId, AudioRoute.SPEAKER)
|
||||
syncProximity()
|
||||
LogManager.addLog("INFO", "call", "video speaker enabled after confirmed FAR id=${st.callId}")
|
||||
}
|
||||
|
||||
/** Только живые события ядра, сериализованные AppEventHandler на main thread. */
|
||||
fun onEvent(type: Int, data: ByteArray?) {
|
||||
checkMain()
|
||||
@@ -342,6 +374,19 @@ object CallController : TelecomCallManager.Listener {
|
||||
owner.audio.router.onChanged = { route, headset -> routeChanged(route, headset) }
|
||||
_earpiece.value = owner.audio.router.hasEarpiece
|
||||
routeChanged(owner.audio.router.route, owner.audio.router.headsetAvailable)
|
||||
owner.video?.let { engine ->
|
||||
videoJob = scope.launch {
|
||||
combine(engine.state, engine.display) { video, display -> video.settings.enabled || display != null }
|
||||
.distinctUntilChanged().collect { active ->
|
||||
if (service !== owner || _state.value?.callId != id || _state.value?.phase != CallPhase.ACTIVE) return@collect
|
||||
if (active == _videoActive.value) return@collect
|
||||
_videoActive.value = active
|
||||
videoSpeakerPending = active && _route.value == AudioRoute.EARPIECE
|
||||
LogManager.addLog("INFO", "call", "video active=$active speaker_pending=$videoSpeakerPending proximity=${proximity.near.value} id=$id")
|
||||
syncProximity(); applyVideoSpeakerIfReady()
|
||||
}
|
||||
}
|
||||
}
|
||||
owner.updateNotification()
|
||||
LogManager.addLog("INFO", "Call", "active id=$id; service owns audio and CPU wake lock")
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -123,6 +124,7 @@ fun CallScreen(
|
||||
val peerNodeId = st?.peerNodeId ?: 0L
|
||||
val name = st?.peerName?.ifEmpty { "0x%016x".format(peerNodeId) } ?: ""
|
||||
val video by CallController.video.collectAsState()
|
||||
val videoActive by CallController.videoActive.collectAsState()
|
||||
val remote = video?.display?.collectAsState()?.value
|
||||
val receiving = phase == CallPhase.ACTIVE && remote != null
|
||||
var controlsVisible by rememberSaveable { mutableStateOf(true) }
|
||||
@@ -132,9 +134,20 @@ fun CallScreen(
|
||||
if (receiving && controlsVisible) { delay(5000); controlsVisible = false }
|
||||
}
|
||||
|
||||
val locked = phase == CallPhase.ACTIVE && route == AudioRoute.EARPIECE && proximityNear
|
||||
val locked = phase == CallPhase.ACTIVE && !videoActive && route == AudioRoute.EARPIECE && proximityNear == true
|
||||
|
||||
val context = LocalContext.current
|
||||
val view = LocalView.current
|
||||
DisposableEffect(view, videoActive) {
|
||||
val previous = view.keepScreenOn
|
||||
if (videoActive) {
|
||||
view.keepScreenOn = true
|
||||
LogManager.addLog("INFO", "call", "video screen awake; proximity overlay disabled")
|
||||
}
|
||||
onDispose {
|
||||
if (videoActive) { view.keepScreenOn = previous; LogManager.addLog("INFO", "call", "video screen wake policy restored") }
|
||||
}
|
||||
}
|
||||
// Ориентация принадлежит экрану звонка; краткое переподключение Surface не должно поворачивать его обратно.
|
||||
DisposableEffect(video, phase == CallPhase.ACTIVE) {
|
||||
val activity = context.findActivity(); val previous = activity?.requestedOrientation
|
||||
|
||||
Reference in new issue
Block a user