Mark a call offer time-critical, so it rings
Reported from a physical iPhone: an incoming call showed no notification and no full-screen call. Android never sent `urgent` on any envelope. The relay cannot work out that an envelope is a call: it holds ciphertext, and an offer and a birthday message are the same bytes to it. So §6.14 makes the sender say, and only an urgent envelope gets a VoIP push. Only a VoIP push rings a locked, suspended iPhone; anything else is a "New message" banner, or nothing at all when the phone is locked. So every call placed from Android reached an iPhone as an ordinary notification, and the call screen never appeared. iOS has set this since the flag existed (`ringsAPhone` in E2EInbox), which is why calls the other way round work. The detection was already here and used for the opposite question: `warrantsAlert` identifies an OFFER to decide whether to make a sound. Now `ringsAPhone` asks the same thing to decide whether to ring, and the two stay separate, an answer or a hangup must not ring a phone for a call that is already over. 4 new tests. 1221 tests pass, counted from the XML. ⚠️ Not yet observed ringing a real locked iPhone; that needs two handsets and is the check to run before believing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1e0bf0ba1f
commit
db9ca2b442
@@ -551,6 +551,19 @@ class Conversations(
|
||||
return content.isRenderable
|
||||
}
|
||||
|
||||
/**
|
||||
* §6.14, must ring a locked phone. A call offer, and nothing else.
|
||||
*
|
||||
* The relay cannot tell: it holds ciphertext, so an offer and a birthday
|
||||
* message are the same bytes to it. Without this the relay sends an
|
||||
* ordinary alert, which on iOS is a "New message" banner rather than the
|
||||
* ringing call screen, and a locked iPhone shows nothing at all.
|
||||
*/
|
||||
private fun ringsAPhone(body: String): Boolean {
|
||||
if (!body.startsWith("""{"fnc":""")) return false
|
||||
return Content.parse(body).call?.action == Content.CallSignal.OFFER
|
||||
}
|
||||
|
||||
private fun sendPlaintext(
|
||||
toUserId: Long,
|
||||
text: String,
|
||||
@@ -694,6 +707,7 @@ class Conversations(
|
||||
outgoing,
|
||||
identity.deviceId,
|
||||
silent = !warrantsAlert(text),
|
||||
urgent = ringsAPhone(text),
|
||||
// The key only when there is no capability.
|
||||
deliveryKey = if (peerToken == null) peerKey?.let(Wire::b64) else null,
|
||||
deliveryToken = peerToken,
|
||||
|
||||
@@ -210,6 +210,13 @@ class RelayClient(
|
||||
* Deliver without announcing: protocol traffic, not a message.
|
||||
*/
|
||||
silent: Boolean = false,
|
||||
/**
|
||||
* §6.14, time-critical: it must ring a locked, suspended phone. Only a
|
||||
* VoIP push does that on iOS, and the relay only sends one when this
|
||||
* is set, so a call offer without it arrives as an ordinary
|
||||
* notification and never rings.
|
||||
*/
|
||||
urgent: Boolean = false,
|
||||
): JSONObject =
|
||||
postJson(
|
||||
"/e2e/messages",
|
||||
@@ -224,6 +231,7 @@ class RelayClient(
|
||||
body.put("from_device", fromDevice)
|
||||
}
|
||||
if (silent) body.put("silent", true)
|
||||
if (urgent) body.put("urgent", true)
|
||||
}.put(
|
||||
"messages",
|
||||
JSONArray().also { array ->
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package io.flamenet.client
|
||||
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* A call offer must be marked time-critical (spec §6.14).
|
||||
*
|
||||
* The relay holds ciphertext, so it cannot tell an offer from a birthday
|
||||
* message. It sends a VoIP push only when the sender says urgent, and only a
|
||||
* VoIP push rings a locked, suspended iPhone. Android set `silent` and never
|
||||
* `urgent`, so an Android call reached an iPhone as an ordinary "New message"
|
||||
* notification and the call screen never appeared.
|
||||
*/
|
||||
class UrgentCallOfferTest {
|
||||
private fun source(path: String): String {
|
||||
val f = File(path)
|
||||
assertTrue(f.exists(), "missing $path")
|
||||
return f.readText()
|
||||
}
|
||||
|
||||
private val conversations = source("src/main/kotlin/io/flamenet/client/Conversations.kt")
|
||||
private val relayClient = source("src/main/kotlin/io/flamenet/client/RelayClient.kt")
|
||||
|
||||
/** The flag reaches the wire. */
|
||||
@Test
|
||||
fun urgentIsSentToTheRelay() {
|
||||
assertTrue(relayClient.contains("urgent: Boolean = false"), "submit must accept it")
|
||||
assertTrue(
|
||||
relayClient.contains("""if (urgent) body.put("urgent", true)"""),
|
||||
"and must put it on the body",
|
||||
)
|
||||
}
|
||||
|
||||
/** And a call offer sets it. */
|
||||
@Test
|
||||
fun aCallOfferIsUrgent() {
|
||||
assertTrue(conversations.contains("private fun ringsAPhone(body: String): Boolean"))
|
||||
assertTrue(
|
||||
conversations.contains("urgent = ringsAPhone(text),"),
|
||||
"the send path must pass it",
|
||||
)
|
||||
assertTrue(
|
||||
conversations.contains("Content.parse(body).call?.action == Content.CallSignal.OFFER"),
|
||||
"only an offer rings",
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Urgent and silent answer different questions. An offer is both loud and
|
||||
* time-critical; an answer or a hangup is neither, and must not ring a
|
||||
* phone for a call that is already over.
|
||||
*/
|
||||
@Test
|
||||
fun theTwoFlagsStaySeparate() {
|
||||
assertTrue(conversations.contains("silent = !warrantsAlert(text),"))
|
||||
assertTrue(conversations.contains("urgent = ringsAPhone(text),"))
|
||||
assertFalse(
|
||||
conversations.contains("urgent = !silent") ||
|
||||
conversations.contains("urgent = warrantsAlert"),
|
||||
"urgent must not be derived from silent",
|
||||
)
|
||||
}
|
||||
|
||||
/** A body that is not an fnc envelope is an ordinary message, and never rings. */
|
||||
@Test
|
||||
fun aPlainMessageNeverRings() {
|
||||
val fn = conversations.substringAfter("private fun ringsAPhone").substringBefore("}")
|
||||
assertTrue(fn.contains("return false"), "must refuse a non-envelope body")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user