diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt b/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt index 93818c1..5d6eb5a 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt @@ -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() { diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerService.kt b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerService.kt index 1f1e50d..6ccbb26 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerService.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerService.kt @@ -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() 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 d0c8f51..e222c9f 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt @@ -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