diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/CallManager.kt b/app/src/main/kotlin/io/flamenet/messenger/app/CallManager.kt index fbc509e..c10395b 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/CallManager.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/CallManager.kt @@ -102,6 +102,35 @@ class CallManager( */ private var ringGuard: Runnable? = null + /** + * Ends a call whose far end stopped answering the media. + * + * `call-end` is the polite path and usually arrives first. When it does not, + * because it was lost or the app was somewhere it could not be read, this is + * what notices. Short, because a call whose audio has stopped is over + * whatever the signalling says. + */ + private var goneGuard: Runnable? = null + + private fun armGoneGuard() { + if (goneGuard != null) return + val guard = + Runnable { + goneGuard = null + if (state == State.ACTIVE || state == State.CONNECTING) { + android.util.Log.i("fnmsg-call", "far end gone, ending the call") + hangUp(notifyPeer = true, reason = "timeout") + } + } + goneGuard = guard + main.postDelayed(guard, GONE_TIMEOUT_MS) + } + + private fun cancelGoneGuard() { + goneGuard?.let { main.removeCallbacks(it) } + goneGuard = null + } + private fun armRingGuard() { cancelRingGuard() val guard = @@ -258,11 +287,22 @@ class CallManager( when (newState) { PeerConnection.IceConnectionState.CONNECTED, PeerConnection.IceConnectionState.COMPLETED, - -> state = State.ACTIVE + -> { + cancelGoneGuard() + state = State.ACTIVE + } // CLOSED is what our OWN close() produces. PeerConnection.IceConnectionState.CLOSED -> Unit + // The far end went away. Transient by definition, + // WebRTC recovers from it often, so this is a grace + // rather than a hang-up: if it comes back the timer + // is cancelled below. Without it a lost `call-end` + // left the screen up until the dial guard gave up + // three quarters of a minute later. + PeerConnection.IceConnectionState.DISCONNECTED -> armGoneGuard() + PeerConnection.IceConnectionState.FAILED -> hangUp(notifyPeer = true) else -> Unit @@ -584,6 +624,8 @@ class CallManager( dialGuard = null connectGuard?.let { main.removeCallbacks(it) } connectGuard = null + cancelGoneGuard() + cancelRingGuard() val active = current if (notifyPeer && active != null) { // Best effort. @@ -646,6 +688,12 @@ class CallManager( * only when their hang-up never reached us. */ const val RING_TIMEOUT_MS = 60_000L + + /** + * How long a connected call may stay disconnected before it is over. + * Long enough for a real blip, short enough not to be a stuck screen. + */ + const val GONE_TIMEOUT_MS = 8_000L const val CONNECT_TIMEOUT_MS = 30_000L /** diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt index e222c9f..2e3dc31 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt @@ -2334,6 +2334,16 @@ class MessengerViewModel( onStateChanged = { state -> // Filed once, at the end. if (state == CallManager.State.ENDED || state == CallManager.State.IDLE) { + // The ring notification belongs to the service, and the + // manager knows nothing of it. A ring that ended here, + // by the caller hanging up or by the guard giving up, + // used to leave it standing: an Answer button for a + // call that no longer existed. Seen on a device. + runCatching { + androidx.core.app.NotificationManagerCompat + .from(getApplication()) + .cancel(3) + } calls?.current?.let { active -> if (!callFiled) { callFiled = true @@ -3457,6 +3467,22 @@ class MessengerViewModel( } fun pausePolling() { + // Never while a call is in flight. This loop is the only thing that + // routes `call-end` into the manager that owns the call screen; the + // service polls too, but it turns offers into notifications and knows + // nothing about a call already up. Stopping here mid-call left the far + // end's hang-up with nowhere to land, so the screen sat there offering + // to answer a call that had gone, until the dial guard gave up 45 + // seconds later. Seen on a device. + when (calls?.state) { + CallManager.State.DIALLING, + CallManager.State.RINGING, + CallManager.State.CONNECTING, + CallManager.State.ACTIVE, + -> return + + else -> Unit + } polling?.cancel() polling = null }