Retain overlay windows through partial IPC failures and guarded service startup

This commit is contained in:
evgeny committed 2026-10-29 13:34:23 +01:00
1 parent fa14305349
commit b3d70ea92e
1 file changed
+55 -36
@@ -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() {