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 95b82ba1..424ac5e4 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 @@ -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) } }