One MainActivity, ever: a stacked instance made hang-up a no-op
The default launch mode stacked a second MainActivity — with a second ViewModel, session and CallManager — whenever the app was re-entered. The visible screen's Hang up then commanded a manager with no call while the real call rang on underneath, and the peer never got the END. singleTask, plus a 30s connect guard so an answered call whose ICE never finds a path gives up out loud on both screens instead of sitting in Connecting forever. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6a0f642e0d
commit
53ba8ea42e
@@ -55,9 +55,20 @@
|
||||
presents as a corrupt install rather than as a missing key.
|
||||
-->
|
||||
|
||||
<!--
|
||||
🔴 singleTask is load-bearing, not cosmetic. The default launch mode
|
||||
stacks a NEW MainActivity — with a NEW ViewModel, session, poll loop
|
||||
and CallManager — every time the app is entered through a
|
||||
notification or a fresh launcher intent. Two instances means the
|
||||
visible screen's Hang up commands a CallManager that has no call,
|
||||
while the real call keeps ringing in the instance underneath: the
|
||||
peer never receives the END, and "I ended the call but their phone
|
||||
kept going" is exactly how it presents.
|
||||
-->
|
||||
<activity
|
||||
android:name=".app.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -113,6 +113,23 @@ class CallManager(
|
||||
/** Gives up an unanswered outgoing call, like iOS's 45 s dial timeout. */
|
||||
private var dialGuard: Runnable? = null
|
||||
|
||||
/**
|
||||
* Gives up a call that was answered but whose media never connected.
|
||||
* Without it, an answer whose ICE cannot find a path — no TURN, hostile
|
||||
* NAT — left BOTH screens sitting in "Connecting…" until someone forced
|
||||
* the app closed.
|
||||
*/
|
||||
private var connectGuard: Runnable? = null
|
||||
|
||||
private fun armConnectGuard() {
|
||||
connectGuard?.let { main.removeCallbacks(it) }
|
||||
val guard = Runnable {
|
||||
if (state == State.CONNECTING) hangUp(notifyPeer = true, reason = "timeout")
|
||||
}
|
||||
connectGuard = guard
|
||||
main.postDelayed(guard, CONNECT_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
/** Main thread only. */
|
||||
private fun sendWhenGathered(send: (String) -> Unit) {
|
||||
onGathered = send
|
||||
@@ -372,6 +389,7 @@ class CallManager(
|
||||
dialGuard?.let { main.removeCallbacks(it) }
|
||||
dialGuard = null
|
||||
state = State.CONNECTING
|
||||
armConnectGuard()
|
||||
connection?.setRemoteDescription(
|
||||
SimpleSdpObserver(),
|
||||
SessionDescription(SessionDescription.Type.ANSWER, sdp),
|
||||
@@ -390,6 +408,7 @@ class CallManager(
|
||||
pendingOffer = null
|
||||
val sdp = offer.sdp ?: return
|
||||
state = State.CONNECTING
|
||||
armConnectGuard()
|
||||
val pc = open(turn, fromUser, offer.callId, offer.video) ?: run {
|
||||
state = State.IDLE
|
||||
return
|
||||
@@ -458,6 +477,8 @@ class CallManager(
|
||||
gatherGuard = null
|
||||
dialGuard?.let { main.removeCallbacks(it) }
|
||||
dialGuard = null
|
||||
connectGuard?.let { main.removeCallbacks(it) }
|
||||
connectGuard = null
|
||||
val active = current
|
||||
if (notifyPeer && active != null) {
|
||||
// Best effort. Telling the other end is worth doing and is not
|
||||
@@ -511,6 +532,7 @@ class CallManager(
|
||||
companion object {
|
||||
/** How long an unanswered outgoing call rings before giving up. */
|
||||
const val DIAL_TIMEOUT_MS = 45_000L
|
||||
const val CONNECT_TIMEOUT_MS = 30_000L
|
||||
|
||||
/**
|
||||
* How long to wait for ICE gathering before sending what we have. An
|
||||
|
||||
Reference in New Issue
Block a user