mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-10-30 09:30:20 +00:00
Retain early remote notifications until tunnel negotiation
This commit is contained in:
parent
dec7b8207a
commit
9525dd3895
@ -52,7 +52,11 @@ device; this is distinct from its delivery target.
|
||||
An older tunnel keeps the existing direct hangup/busy cleanup and notification
|
||||
filtering. An older CLI ignores the added capability and target fields. Either
|
||||
project can upgrade independently; complete multi-device behavior requires both.
|
||||
The account's local device-ID fix is independent of this protocol extension.
|
||||
Linked accounts also require the local device-ID correction included in this branch.
|
||||
Notifications received before capability negotiation are retained until the version
|
||||
is known. An unversioned `ready` alone does not settle negotiation: a subsequent
|
||||
capability event enables version 2; a first legacy signaling/state event selects
|
||||
legacy behavior.
|
||||
|
||||
## Spawning the Tunnel
|
||||
|
||||
|
||||
@ -276,7 +276,7 @@ public class CallManager implements AutoCloseable {
|
||||
answerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
|
||||
sendControlMessage(state, writeJson(answerMsg));
|
||||
|
||||
if (!state.multiDeviceSignaling) {
|
||||
if (!Boolean.TRUE.equals(state.multiDeviceSignaling)) {
|
||||
state.deviceId = deviceId;
|
||||
}
|
||||
if (state.state != CallInfo.State.CONNECTED && state.state != CallInfo.State.RECONNECTING) {
|
||||
@ -310,7 +310,13 @@ public class CallManager implements AutoCloseable {
|
||||
if (state == null || !state.recipientId.equals(sender)) {
|
||||
return;
|
||||
}
|
||||
if (!state.multiDeviceSignaling) {
|
||||
synchronized (state) {
|
||||
if (state.multiDeviceSignaling == null && type != null && type != HangupMessage.Type.NORMAL) {
|
||||
state.pendingRemoteNotifications.add(() -> handleIncomingHangup(sender, callId, senderDeviceId, type, deviceId));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!Boolean.TRUE.equals(state.multiDeviceSignaling)) {
|
||||
if (type == null || type == HangupMessage.Type.NORMAL) {
|
||||
endCall(callId, "remote_hangup");
|
||||
}
|
||||
@ -335,7 +341,7 @@ public class CallManager implements AutoCloseable {
|
||||
if (state == null || !state.recipientId.equals(sender)) {
|
||||
return;
|
||||
}
|
||||
if (!state.multiDeviceSignaling) {
|
||||
if (!Boolean.TRUE.equals(state.multiDeviceSignaling)) {
|
||||
endCall(callId, "remote_busy");
|
||||
return;
|
||||
}
|
||||
@ -500,6 +506,17 @@ public class CallManager implements AutoCloseable {
|
||||
return writeJson(config);
|
||||
}
|
||||
|
||||
private void setSignalingVersion(CallState state, int version) {
|
||||
synchronized (state) {
|
||||
state.multiDeviceSignaling = version >= 2;
|
||||
var pending = List.copyOf(state.pendingRemoteNotifications);
|
||||
state.pendingRemoteNotifications.clear();
|
||||
for (var notification : pending) {
|
||||
notification.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void readControlEvents(CallState state, java.io.InputStream inputStream) {
|
||||
try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
@ -512,9 +529,17 @@ public class CallManager implements AutoCloseable {
|
||||
var json = mapper.readTree(line);
|
||||
var type = json.has("type") ? json.get("type").asText() : "";
|
||||
|
||||
if (state.multiDeviceSignaling == null && switch (type) {
|
||||
case "sendOffer", "sendAnswer", "sendIce", "sendHangup", "sendBusy", "stateChange" -> true;
|
||||
default -> false;
|
||||
}) {
|
||||
setSignalingVersion(state, 1);
|
||||
}
|
||||
switch (type) {
|
||||
case "ready" -> {
|
||||
state.multiDeviceSignaling = json.path("signalingVersion").asInt(1) >= 2;
|
||||
if (json.has("signalingVersion")) {
|
||||
setSignalingVersion(state, json.path("signalingVersion").asInt(1));
|
||||
}
|
||||
if (json.has("inputDeviceName")) {
|
||||
state.inputDeviceName = json.get("inputDeviceName").asText();
|
||||
}
|
||||
@ -527,7 +552,7 @@ public class CallManager implements AutoCloseable {
|
||||
state.outputDeviceName);
|
||||
}
|
||||
case "signalingCapabilities" -> {
|
||||
state.multiDeviceSignaling = json.path("version").asInt(1) >= 2;
|
||||
setSignalingVersion(state, json.path("version").asInt(1));
|
||||
}
|
||||
case "sendOffer" -> {
|
||||
var opaqueB64 = json.get("opaque").asText();
|
||||
@ -553,7 +578,7 @@ public class CallManager implements AutoCloseable {
|
||||
var hangupType = json.has("hangupType")
|
||||
? json.get("hangupType").asText("normal")
|
||||
: "normal";
|
||||
if (!state.multiDeviceSignaling && hangupType.contains("onanotherdevice")) {
|
||||
if (!Boolean.TRUE.equals(state.multiDeviceSignaling) && hangupType.contains("onanotherdevice")) {
|
||||
break;
|
||||
}
|
||||
logSendMessageResult(sendHangupViaSignal(state, hangupType,
|
||||
@ -818,7 +843,9 @@ public class CallManager implements AutoCloseable {
|
||||
volatile CallInfo.State state;
|
||||
final RecipientId recipientId;
|
||||
volatile Integer deviceId;
|
||||
volatile boolean multiDeviceSignaling;
|
||||
// Unknown until capabilities or the first legacy call event arrive.
|
||||
volatile Boolean multiDeviceSignaling;
|
||||
final List<Runnable> pendingRemoteNotifications = new ArrayList<>();
|
||||
final boolean isOutgoing;
|
||||
volatile String inputDeviceName;
|
||||
volatile String outputDeviceName;
|
||||
|
||||
@ -70,6 +70,46 @@ class CallManagerSignalingTest {
|
||||
method.invoke(manager, state, new ByteArrayInputStream((json + "\n").getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaysAcceptedNotificationReceivedBeforeReady() throws Exception {
|
||||
manager.handleIncomingHangup(state.recipientId, state.callId, 1, HangupMessage.Type.ACCEPTED, 2);
|
||||
assertEquals("", output.toString());
|
||||
events("{\"type\":\"ready\",\"signalingVersion\":2}");
|
||||
var message = MAPPER.readTree(output.toString());
|
||||
assertEquals("receivedHangup", message.path("type").asText());
|
||||
assertEquals(1, message.path("hangupType").asInt());
|
||||
assertEquals(2, message.path("deviceId").asInt());
|
||||
assertTrue(state.pendingRemoteNotifications.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void separateCapabilitiesDoNotLoseEarlyNotification() throws Exception {
|
||||
manager.handleIncomingHangup(state.recipientId, state.callId, 1, HangupMessage.Type.DECLINED, 2);
|
||||
events("{\"type\":\"ready\"}");
|
||||
assertEquals("", output.toString());
|
||||
events("{\"type\":\"signalingCapabilities\",\"version\":2}");
|
||||
assertEquals(2, MAPPER.readTree(output.toString()).path("hangupType").asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyCallEventResolvesPendingNotification() throws Exception {
|
||||
manager.handleIncomingHangup(state.recipientId, state.callId, 1, HangupMessage.Type.ACCEPTED, 2);
|
||||
events("{\"type\":\"ready\"}\n{\"type\":\"stateChange\",\"state\":\"Connected\"}");
|
||||
assertEquals(Boolean.FALSE, state.multiDeviceSignaling);
|
||||
assertTrue(state.pendingRemoteNotifications.isEmpty());
|
||||
assertEquals("", output.toString());
|
||||
assertEquals(CallInfo.State.CONNECTED, state.state);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotReplayNotificationsAfterCallRemoval() throws Exception {
|
||||
manager.handleIncomingHangup(state.recipientId, state.callId, 1, HangupMessage.Type.ACCEPTED, 2);
|
||||
calls.clear();
|
||||
events("{\"type\":\"ready\",\"signalingVersion\":2}");
|
||||
assertEquals("", output.toString());
|
||||
assertTrue(state.pendingRemoteNotifications.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readyNegotiatesBeforeCallEvents() throws Exception {
|
||||
events("{\"type\":\"ready\",\"signalingVersion\":2}");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user