Cancel overlay PTT on gesture faults and ignore callbacks from retired windows

This commit is contained in:
evgeny committed 2026-10-29 13:34:23 +01:00
1 parent b3d70ea92e
commit 982315bacc
1 file changed
+54 -29
@@ -392,7 +392,7 @@ class RadioOverlayService : Service() {
.build()
private fun addOverlay() {
val wm = windowManager ?: return
val wm = requireNotNull(windowManager) { "window manager unavailable" }
val v = TextView(this).apply {
text = "\uD83C\uDF99"
textSize = 21f
@@ -430,6 +430,12 @@ class RadioOverlayService : Service() {
private var secondTap = false
override fun onTouch(view: View, event: MotionEvent): Boolean {
if (destroyed || currentService !== this@RadioOverlayService || overlayView !== v || !overlayReady || pendingStop) return false
if (!MicrophonePermission.granted() || !ChatApplication.instance.configProvider.getRadioOverlay() || !canDrawOverlays(this@RadioOverlayService)) {
removeOverlay()
return false
}
try {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startX = params?.x ?: lp.x
@@ -482,46 +488,58 @@ class RadioOverlayService : Service() {
MotionEvent.ACTION_CANCEL -> {
latched = false
lastTapUpTime = 0L
endTalk()
endTalk(immediate = true)
return true
}
}
return false
} catch (e: Exception) {
latched = false; lastTapUpTime = 0L
removeOverlay()
setStatus(R.string.overlay_show_failed)
LogManager.addLog("ERROR", "RadioOverlay", "Overlay touch failed action=${event.action} x=${event.rawX} " +
"y=${event.rawY}; own PTT cancelled: $e")
return true
}
}
})
wm.addView(v, lp)
overlayView = v
params = lp
overlayReady = false
_isShown.value = true // addView может бросить после добавления окна: до rollback считаем окно возможным.
wm.addView(v, lp)
overlayReady = true
positionDragged = false
placeAtDefaultCenter()
_isShown.value = true
LogManager.addLog("INFO", "RadioOverlay", "overlay shown")
LogManager.addLog("INFO", "RadioOverlay", "overlay shown view=${System.identityHashCode(v)} x=${lp.x} y=${lp.y}")
}
/* Поправка по фактическим координатам окна учитывает системные отступы оверлея. */
private fun placeAtDefaultCenter() {
if (destroyed || currentService !== this || !overlayReady) return
val v = overlayView ?: return
if (positionDragged) return
if (!v.isLaidOut) {
v.doOnLayout { placeAtDefaultCenter() }
v.doOnLayout { if (overlayView === v && !destroyed) placeAtDefaultCenter() }
return
}
if (positioningPending) return
positioningPending = true
v.postOnAnimation {
if (destroyed || currentService !== this || overlayView !== v || !overlayReady) return@postOnAnimation
positioningPending = false
val target = defaultCenter ?: return@postOnAnimation
val lp = params ?: return@postOnAnimation
if (overlayView !== v || positionDragged) return@postOnAnimation
val actual = IntArray(2)
v.getLocationOnScreen(actual)
val dx = target.x - actual[0] - v.width / 2
val dy = target.y - actual[1] - v.height / 2
if (dx == 0 && dy == 0) return@postOnAnimation
lp.x += dx
lp.y += dy
try {
windowManager?.updateViewLayout(v, lp)
val actual = IntArray(2)
v.getLocationOnScreen(actual)
val dx = target.x - actual[0] - v.width / 2
val dy = target.y - actual[1] - v.height / 2
if (dx == 0 && dy == 0) return@postOnAnimation
lp.x += dx
lp.y += dy
requireNotNull(windowManager) { "window manager unavailable" }.updateViewLayout(v, lp)
LogManager.addLog("DEBUG", "RadioOverlay", "default position center=${target.x},${target.y} x=${lp.x} y=${lp.y}")
} catch (e: Exception) {
LogManager.addLog("ERROR", "RadioOverlay", "default position failed: ${e.message}")
@@ -536,10 +554,11 @@ class RadioOverlayService : Service() {
updateButtonColor()
}
private fun endTalk() {
if (!talking) return
private fun endTalk(immediate: Boolean = false) {
if (!talking && !immediate) return
talking = false
RadioAudioEngine.instance().talkEnd(RadioPttSource.OVERLAY)
if (immediate) RadioAudioEngine.instance().cancelTalk(RadioPttSource.OVERLAY)
else RadioAudioEngine.instance().talkEnd(RadioPttSource.OVERLAY)
updateButtonColor()
}
@@ -552,21 +571,27 @@ class RadioOverlayService : Service() {
private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt()
override fun onDestroy() {
checkMain()
if (destroyed) return
destroyed = true
val owner = currentService === this
serviceScope.cancel()
removeOverlay()
starting = false
running = false
pendingStop = false
if (currentService === this) currentService = null
_isShown.value = false
if (_status.value == R.string.overlay_active || _status.value == R.string.overlay_starting) {
setStatus(if (ChatApplication.instance.configProvider.getRadioOverlay()) R.string.overlay_stopped else null)
}
LogManager.addLog("INFO", "RadioOverlay", "overlay destroyed")
super.onDestroy()
if (restartAfterStop) {
var restart = false
if (owner) {
starting = false; running = false; pendingStop = false
if (overlayView == null) { currentService = null; windowManager = null }
if (_status.value == R.string.overlay_active || _status.value == R.string.overlay_starting) {
setStatus(if (ChatApplication.instance.configProvider.getRadioOverlay()) R.string.overlay_stopped else null)
}
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (e: Exception) {
LogManager.addLog("ERROR", "RadioOverlay", "Foreground notification removal failed id=$NOTIF_ID: $e")
}
restart = restartAfterStop
restartAfterStop = false
sync(ChatApplication.instance)
}
LogManager.addLog("INFO", "RadioOverlay", "overlay destroyed owner=$owner windowRetained=${overlayView != null} restart=$restart")
super.onDestroy()
if (restart) sync(ChatApplication.instance)
}
}