From b3d70ea92e45e409c23d3d4d463434dbe58dcf6c Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 19:33:35 +0100 Subject: [PATCH] Retain overlay windows through partial IPC failures and guarded service startup --- .../com/utun/chat/data/RadioOverlayService.kt | 91 +++++++++++-------- 1 file changed, 55 insertions(+), 36 deletions(-) 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 115f14d9..95b82ba1 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 @@ -279,80 +279,99 @@ class RadioOverlayService : Service() { override fun onBind(intent: Intent?): IBinder? = null override fun onCreate() { + checkMain() super.onCreate() - LogManager.addLog("DEBUG", "RADIO_DBG", "RadioOverlayService.onCreate thread=${Thread.currentThread().name} t=${System.currentTimeMillis()}") - createChannel() + var operation = "channel" try { + createChannel() + operation = "foreground" 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()) } - } catch (e: Exception) { - LogManager.addLog("ERROR", "RadioOverlay", "Foreground microphone start failed: $e") - starting = false - setStatus(R.string.microphone_service_failed) - stopSelf() - return - } - - running = true - currentService = this - windowManager = getSystemService(WINDOW_SERVICE) as WindowManager - serviceScope.launch { - RadioAudioEngine.instance().talkingFlow.collect { updateButtonColor() } - } - starting = false - LogManager.addLog("INFO", "RadioOverlay", "Foreground microphone service ready") - RadioAudioEngine.instance().reconfigure() - serviceScope.launch { - while (isActive) { - delay(1000) - if (!MicrophonePermission.granted() || - (overlayView != null && !canDrawOverlays(this@RadioOverlayService))) sync(this@RadioOverlayService) + operation = "previous owner" + currentService?.takeIf { it !== this }?.let { previous -> + check(previous.destroyed && previous.removeOverlay()) { "previous overlay owner still retained" } } - } - if (pendingStop) { + currentService = this + running = true + starting = false + if (pendingStop) { + LogManager.addLog("INFO", "RadioOverlay", "Cancelled service start completed foreground handshake") + stopSelf(); return + } + operation = "window service" + windowManager = getSystemService(WINDOW_SERVICE) as? WindowManager ?: error("window manager unavailable") + serviceScope.launch { RadioAudioEngine.instance().talkingFlow.collect { updateButtonColor() } } + RadioAudioEngine.instance().reconfigure() + serviceScope.launch { + while (isActive) { + delay(1000) + if (!MicrophonePermission.granted() || + (overlayView != null && !canDrawOverlays(this@RadioOverlayService))) sync(this@RadioOverlayService) + } + } + LogManager.addLog("INFO", "RadioOverlay", "Foreground microphone service ready notification=$NOTIF_ID") + } catch (e: Exception) { + LogManager.addLog("ERROR", "RadioOverlay", "service setup failed operation=$operation: $e") + if (currentService == null) currentService = this + if (currentService === this) { starting = false; running = false; pendingStop = true } + setStatus(R.string.microphone_service_failed) stopSelf() } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + checkMain() refresh() return START_NOT_STICKY } private fun refresh() { - if (!running || pendingStop || !RadioAudioEngine.instance().isActive() || !MicrophonePermission.granted()) { + if (destroyed || currentService !== this || !running || pendingStop || + !RadioAudioEngine.instance().isActive() || !MicrophonePermission.granted()) { removeOverlay() + if (currentService === this) pendingStop = true stopSelf() return } val provider = ChatApplication.instance.configProvider val wantOverlay = provider.getRadioOverlay() && canDrawOverlays(this) + if (overlayView != null && !overlayReady && !removeOverlay()) return if (wantOverlay && overlayView == null) { try { addOverlay() } catch (e: Exception) { + removeOverlay() setStatus(R.string.overlay_show_failed) LogManager.addLog("ERROR", "RadioOverlay", "Overlay creation failed: $e") + if (provider.getInt("radio.auto_enabled") == 0) { pendingStop = true; stopSelf() } + return } } if (!wantOverlay) removeOverlay() - if (overlayView != null) setStatus(R.string.overlay_active) - if (!wantOverlay && provider.getInt("radio.auto_enabled") == 0) stopSelf() + if (overlayReady) setStatus(R.string.overlay_active) + if (!wantOverlay && provider.getInt("radio.auto_enabled") == 0) { pendingStop = true; stopSelf() } } - private fun removeOverlay() { - val view = overlayView ?: return + /** Снять только свой PTT. Ссылка на окно сохраняется до успешного remove либо подтверждённого отсутствия. */ + private fun removeOverlay(): Boolean { talking = false - RadioAudioEngine.instance().cancelTalk(RadioPttSource.OVERLAY) - try { windowManager?.removeView(view) } catch (e: Exception) { - LogManager.addLog("WARN", "RadioOverlay", "Overlay removal failed: $e") + overlayReady = false + positioningPending = false + if (currentService === this) RadioAudioEngine.instance().cancelTalk(RadioPttSource.OVERLAY) + val view = overlayView ?: return true + try { requireNotNull(windowManager) { "owned window without manager" }.removeView(view) } catch (e: IllegalArgumentException) { + LogManager.addLog("WARN", "RadioOverlay", "Overlay already absent view=${System.identityHashCode(view)}: $e") + } catch (e: Exception) { + LogManager.addLog("ERROR", "RadioOverlay", "Overlay removal failed view=${System.identityHashCode(view)}; retained for retry: $e") + return false } overlayView = null params = null - _isShown.value = false - LogManager.addLog("INFO", "RadioOverlay", "Overlay removed; transmission cancelled") + if (currentService === this) _isShown.value = false + LogManager.addLog("INFO", "RadioOverlay", "Overlay removed view=${System.identityHashCode(view)}; own transmission cancelled") + return true } private fun createChannel() {