Accept on the call notification answers the call

The notification's Accept button has carried EXTRA_ANSWER_CALL since it
was written, and nothing ever read it. Pressing Accept did what any
other tap did: it opened the app. Answering was left to the next turn of
the polling loop, which refuses a parked offer older than the caller's
patience, so a call taken a little late opened the app onto nothing and
looked like it had dropped the moment you answered it.

MainActivity reads the extra now, on a cold start and on a warm one,
and answers at once rather than waiting for a tick.

The notification also goes away on its own when the offer it shows has
expired. Before, it could outlive the offer by the gap between service
sweeps, which is longer since the loop started holding its poll open,
and during that gap it was a button that could only disappoint.

Seen on the emulator: Accept pressed, app opened, nothing on screen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-10 13:03:04 -04:00
co-authored by Claude Fable 5.1
parent 93d7eaa980
commit 63f2758d46
3 changed files with 60 additions and 0 deletions
@@ -1,5 +1,7 @@
package io.flamenet.messenger.app
import android.content.Intent
import androidx.activity.viewModels
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -152,10 +154,36 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
}
}
private val model: MessengerViewModel by viewModels()
/**
* Accept, pressed on the notification, has to answer the call.
*
* The intent has carried `EXTRA_ANSWER_CALL` since the notification was
* written and nothing read it, so pressing Accept only opened the app and
* left the answering to a polling tick that refuses a stale offer. The call
* then looked like it had dropped the moment you took it.
*/
private fun answerIfAsked(intent: Intent?) {
if (intent?.getBooleanExtra(MessengerService.EXTRA_ANSWER_CALL, false) == true) {
intent.removeExtra(MessengerService.EXTRA_ANSWER_CALL)
model.answerParkedCall()
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
answerIfAsked(intent)
}
// The handover: exactly one poller at a time.
override fun onStart() {
super.onStart()
MessengerService.stop(this)
// Also on the way in: a cold start from the notification arrives here
// rather than through onNewIntent.
answerIfAsked(intent)
}
override fun onStop() {
@@ -310,6 +310,12 @@ class MessengerService : Service() {
// accident while reaching for the torch.
.setOngoing(true)
.setAutoCancel(false)
// Goes away by itself when the offer it is showing has expired.
// `take` refuses a parked offer older than the caller's own
// patience, so a notification that outlived that was a button
// which opened the app onto nothing and read as a dropped call.
// Android cancels this without needing the service to notice.
.setTimeoutAfter(CallManager.DIAL_TIMEOUT_MS)
.setFullScreenIntent(full, true)
.setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, decline, full))
.build()
@@ -3430,6 +3430,32 @@ class MessengerViewModel(
* arrived, `CallManager` moved to RINGING, and the only notification posted
* was the service's own "running" notice.
*/
/**
* Answer the call the notification is showing, now.
*
* The notification's Accept button has always carried
* `EXTRA_ANSWER_CALL`, and nothing ever read it. Tapping Accept therefore
* did what any other tap did: it opened the app. Answering was left to the
* next turn of the polling loop, which refuses a parked offer older than
* the caller's patience, so a call answered a little late opened the app
* onto nothing and looked like it had dropped.
*/
fun answerParkedCall() {
val parked = PendingIncomingCall.take() ?: return
runCatching {
androidx.core.app.NotificationManagerCompat
.from(getApplication())
.cancel(3)
}
viewModelScope.launch(Dispatchers.IO) {
val manager = callManager()
val turn = session?.turnCredentials()
manager.onSignal(parked.fromUser, parked.signal, turn, parked.at)
// The offer only sets it ringing; this is the answer itself.
manager.accept(turn)
}
}
fun pausePolling() {
polling?.cancel()
polling = null