Two bugs found by calling a real emulator and watching what it did.
An unanswered ring never ended. Dialling and connecting each had a
timeout and ringing did not, so a call nobody picked up stayed RINGING
until the app was killed. Nothing showed it, because the notification
can be brushed off the top of the screen while the state behind it
survives, and the phone then answered every later caller "busy" while
looking completely idle. A guard now releases it after sixty seconds,
deliberately longer than the caller's own forty-five, so this end gives
up on its own only when their hang-up never arrived. It hangs up with
"timeout", which the far end already renders as "No answer"; an
invented word would have shown a flat "Call ended".
And the handover was half-built. MainActivity starts the background
service when the screen stops and stops it when the screen starts, under
a comment reading "exactly one poller at a time". Only the service half
existed. The view model's loop kept running beside it, both sessions
read the same inbox, and an envelope is consumed once, so whichever
asked first won it.
When the view model won a call offer the phone rang inside a UI nobody
was looking at, and the service, the only half that can post a
notification, never saw the call. Seen on a device: the offer arrived,
CallManager moved to RINGING, and the only notification posted was the
service's own "running" notice. That is why calls did not announce
themselves with the app off screen.
The loop now stops on ON_STOP and starts again on ON_START, so the
comment is finally true.
Verified on the emulator against relay1: an unanswered offer at
12:51:07 released itself at 12:52:07 and told the caller, where before
it would have refused every call after it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>