1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Log;
4import androidx.annotation.NonNull;
5import com.google.common.base.MoreObjects;
6import com.google.common.base.Objects;
7import com.google.common.base.Preconditions;
8import com.google.common.collect.ImmutableList;
9import com.google.common.collect.ImmutableMap;
10import eu.siacs.conversations.Config;
11import eu.siacs.conversations.entities.Account;
12import eu.siacs.conversations.entities.Contact;
13import eu.siacs.conversations.entities.Message;
14import eu.siacs.conversations.services.XmppConnectionService;
15import eu.siacs.conversations.xmpp.Jid;
16import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
17import eu.siacs.conversations.xmpp.manager.DiscoManager;
18import im.conversations.android.xmpp.model.jingle.Jingle;
19import im.conversations.android.xmpp.model.stanza.Iq;
20import java.util.Arrays;
21import java.util.Collection;
22import java.util.List;
23import java.util.Map;
24import java.util.function.Consumer;
25
26public abstract class AbstractJingleConnection {
27
28 public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
29 public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-";
30
31 protected static final List<State> TERMINATED =
32 Arrays.asList(
33 State.ACCEPTED,
34 State.REJECTED,
35 State.REJECTED_RACED,
36 State.RETRACTED,
37 State.RETRACTED_RACED,
38 State.TERMINATED_SUCCESS,
39 State.TERMINATED_DECLINED_OR_BUSY,
40 State.TERMINATED_CONNECTIVITY_ERROR,
41 State.TERMINATED_CANCEL_OR_TIMEOUT,
42 State.TERMINATED_APPLICATION_FAILURE,
43 State.TERMINATED_SECURITY_ERROR);
44
45 private static final Map<State, Collection<State>> VALID_TRANSITIONS;
46
47 static {
48 final ImmutableMap.Builder<State, Collection<State>> transitionBuilder =
49 new ImmutableMap.Builder<>();
50 transitionBuilder.put(
51 State.NULL,
52 ImmutableList.of(
53 State.PROPOSED,
54 State.SESSION_INITIALIZED,
55 State.TERMINATED_APPLICATION_FAILURE,
56 State.TERMINATED_SECURITY_ERROR));
57 transitionBuilder.put(
58 State.PROPOSED,
59 ImmutableList.of(
60 State.ACCEPTED,
61 State.PROCEED,
62 State.REJECTED,
63 State.RETRACTED,
64 State.TERMINATED_APPLICATION_FAILURE,
65 State.TERMINATED_SECURITY_ERROR,
66 State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection
67 // rebinds
68 ));
69 transitionBuilder.put(
70 State.PROCEED,
71 ImmutableList.of(
72 State.REJECTED_RACED,
73 State.RETRACTED_RACED,
74 State.SESSION_INITIALIZED_PRE_APPROVED,
75 State.TERMINATED_SUCCESS,
76 State.TERMINATED_APPLICATION_FAILURE,
77 State.TERMINATED_SECURITY_ERROR,
78 State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error
79 // bounces of the proceed message
80 ));
81 transitionBuilder.put(
82 State.SESSION_INITIALIZED,
83 ImmutableList.of(
84 State.SESSION_ACCEPTED,
85 State.TERMINATED_SUCCESS,
86 State.TERMINATED_DECLINED_OR_BUSY,
87 State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
88 // and IQ timeouts
89 State.TERMINATED_CANCEL_OR_TIMEOUT,
90 State.TERMINATED_APPLICATION_FAILURE,
91 State.TERMINATED_SECURITY_ERROR));
92 transitionBuilder.put(
93 State.SESSION_INITIALIZED_PRE_APPROVED,
94 ImmutableList.of(
95 State.SESSION_ACCEPTED,
96 State.TERMINATED_SUCCESS,
97 State.TERMINATED_DECLINED_OR_BUSY,
98 State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
99 // and IQ timeouts
100 State.TERMINATED_CANCEL_OR_TIMEOUT,
101 State.TERMINATED_APPLICATION_FAILURE,
102 State.TERMINATED_SECURITY_ERROR));
103 transitionBuilder.put(
104 State.SESSION_ACCEPTED,
105 ImmutableList.of(
106 State.TERMINATED_SUCCESS,
107 State.TERMINATED_DECLINED_OR_BUSY,
108 State.TERMINATED_CONNECTIVITY_ERROR,
109 State.TERMINATED_CANCEL_OR_TIMEOUT,
110 State.TERMINATED_APPLICATION_FAILURE,
111 State.TERMINATED_SECURITY_ERROR));
112 VALID_TRANSITIONS = transitionBuilder.build();
113 }
114
115 final JingleConnectionManager jingleConnectionManager;
116 protected final XmppConnectionService xmppConnectionService;
117 protected final Id id;
118 private final Jid initiator;
119
120 protected State state = State.NULL;
121
122 AbstractJingleConnection(
123 final JingleConnectionManager jingleConnectionManager,
124 final Id id,
125 final Jid initiator) {
126 this.jingleConnectionManager = jingleConnectionManager;
127 this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService();
128 this.id = id;
129 this.initiator = initiator;
130 }
131
132 public Id getId() {
133 return id;
134 }
135
136 boolean isInitiator() {
137 return initiator.equals(id.account.getJid());
138 }
139
140 boolean isResponder() {
141 return !initiator.equals(id.account.getJid());
142 }
143
144 public State getState() {
145 return this.state;
146 }
147
148 protected synchronized boolean isInState(State... state) {
149 return Arrays.asList(state).contains(this.state);
150 }
151
152 protected boolean transition(final State target) {
153 return transition(target, null);
154 }
155
156 protected synchronized boolean transition(final State target, final Runnable runnable) {
157 final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
158 if (validTransitions != null && validTransitions.contains(target)) {
159 this.state = target;
160 if (runnable != null) {
161 runnable.run();
162 }
163 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
164 return true;
165 } else {
166 return false;
167 }
168 }
169
170 protected void transitionOrThrow(final State target) {
171 if (!transition(target)) {
172 throw new IllegalStateException(
173 String.format("Unable to transition from %s to %s", this.state, target));
174 }
175 }
176
177 boolean isTerminated() {
178 return TERMINATED.contains(this.state);
179 }
180
181 abstract void deliverPacket(Iq jinglePacket);
182
183 protected void receiveOutOfOrderAction(final Iq jinglePacket, final Jingle.Action action) {
184 Log.d(
185 Config.LOGTAG,
186 String.format(
187 "%s: received %s even though we are in state %s",
188 id.account.getJid().asBareJid(), action, getState()));
189 if (isTerminated()) {
190 Log.d(
191 Config.LOGTAG,
192 String.format(
193 "%s: got a reason to terminate with out-of-order. but already in state"
194 + " %s",
195 id.account.getJid().asBareJid(), getState()));
196 respondWithOutOfOrder(jinglePacket);
197 } else {
198 terminateWithOutOfOrder(jinglePacket);
199 }
200 }
201
202 protected void terminateWithOutOfOrder(final Iq jinglePacket) {
203 Log.d(
204 Config.LOGTAG,
205 id.account.getJid().asBareJid() + ": terminating session with out-of-order");
206 terminateTransport();
207 transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
208 respondWithOutOfOrder(jinglePacket);
209 this.finish();
210 }
211
212 protected void finish() {
213 if (isTerminated()) {
214 this.jingleConnectionManager.finishConnectionOrThrow(this);
215 } else {
216 throw new AssertionError(String.format("Unable to call finish from %s", this.state));
217 }
218 }
219
220 protected abstract void terminateTransport();
221
222 abstract void notifyRebound();
223
224 protected void sendSessionTerminate(
225 final Reason reason, final String text, final Consumer<State> trigger) {
226 final State previous = this.state;
227 final State target = reasonToState(reason);
228 transitionOrThrow(target);
229 if (previous != State.NULL && trigger != null) {
230 trigger.accept(target);
231 }
232 final var iq = new Iq(Iq.Type.SET);
233 final var jinglePacket =
234 iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId));
235 jinglePacket.setReason(reason, text);
236 send(iq);
237 finish();
238 }
239
240 protected void send(final Iq jinglePacket) {
241 jinglePacket.setTo(id.with);
242 xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
243 }
244
245 protected void respondOk(final Iq jinglePacket) {
246 xmppConnectionService.sendIqPacket(
247 id.account, jinglePacket.generateResponse(Iq.Type.RESULT), null);
248 }
249
250 protected void respondWithTieBreak(final Iq jinglePacket) {
251 respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
252 }
253
254 protected void respondWithOutOfOrder(final Iq jinglePacket) {
255 respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
256 }
257
258 protected void respondWithItemNotFound(final Iq jinglePacket) {
259 respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
260 }
261
262 private void respondWithJingleError(
263 final Iq original, String jingleCondition, String condition, String conditionType) {
264 jingleConnectionManager.respondWithJingleError(
265 id.account, original, jingleCondition, condition, conditionType);
266 }
267
268 private synchronized void handleIqResponse(final Iq response) {
269 if (response.getType() == Iq.Type.ERROR) {
270 handleIqErrorResponse(response);
271 return;
272 }
273 if (response.getType() == Iq.Type.TIMEOUT) {
274 handleIqTimeoutResponse(response);
275 }
276 }
277
278 protected void handleIqErrorResponse(final Iq response) {
279 Preconditions.checkArgument(response.getType() == Iq.Type.ERROR);
280 final String errorCondition = response.getErrorCondition();
281 Log.d(
282 Config.LOGTAG,
283 id.account.getJid().asBareJid()
284 + ": received IQ-error from "
285 + response.getFrom()
286 + " in RTP session. "
287 + errorCondition);
288 if (isTerminated()) {
289 Log.i(
290 Config.LOGTAG,
291 id.account.getJid().asBareJid()
292 + ": ignoring error because session was already terminated");
293 return;
294 }
295 this.terminateTransport();
296 final State target;
297 if (Arrays.asList(
298 "service-unavailable",
299 "recipient-unavailable",
300 "remote-server-not-found",
301 "remote-server-timeout")
302 .contains(errorCondition)) {
303 target = State.TERMINATED_CONNECTIVITY_ERROR;
304 } else {
305 target = State.TERMINATED_APPLICATION_FAILURE;
306 }
307 transitionOrThrow(target);
308 this.finish();
309 }
310
311 protected void handleIqTimeoutResponse(final Iq response) {
312 Preconditions.checkArgument(response.getType() == Iq.Type.TIMEOUT);
313 Log.d(
314 Config.LOGTAG,
315 id.account.getJid().asBareJid()
316 + ": received IQ timeout in RTP session with "
317 + id.with
318 + ". terminating with connectivity error");
319 if (isTerminated()) {
320 Log.i(
321 Config.LOGTAG,
322 id.account.getJid().asBareJid()
323 + ": ignoring error because session was already terminated");
324 return;
325 }
326 this.terminateTransport();
327 transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
328 this.finish();
329 }
330
331 protected boolean remoteHasFeature(final String feature) {
332 final var connection = id.account.getXmppConnection();
333 if (connection == null) {
334 return false;
335 }
336 final var infoQuery = connection.getManager(DiscoManager.class).get(id.with);
337 if (infoQuery == null) {
338 return false;
339 }
340 return infoQuery.hasFeature(feature);
341 }
342
343 public static class Id {
344 public final Account account;
345 public final Jid with;
346 public final String sessionId;
347
348 private Id(final Account account, final Jid with, final String sessionId) {
349 Preconditions.checkNotNull(account);
350 Preconditions.checkNotNull(with);
351 Preconditions.checkNotNull(sessionId);
352 this.account = account;
353 this.with = with;
354 this.sessionId = sessionId;
355 }
356
357 public static Id of(Account account, Iq iq, final Jingle jingle) {
358 return new Id(account, iq.getFrom(), jingle.getSessionId());
359 }
360
361 public static Id of(Account account, Jid with, final String sessionId) {
362 return new Id(account, with, sessionId);
363 }
364
365 public static Id of(final Account account, final Jid with) {
366 return new Id(account, with, JingleConnectionManager.nextRandomId());
367 }
368
369 public static Id of(final Message message) {
370 return new Id(
371 message.getConversation().getAccount(),
372 message.getCounterpart(),
373 message.getUuid());
374 }
375
376 public Contact getContact() {
377 return account.getRoster().getContact(with);
378 }
379
380 @Override
381 public boolean equals(Object o) {
382 if (this == o) return true;
383 if (o == null || getClass() != o.getClass()) return false;
384 Id id = (Id) o;
385 return Objects.equal(account.getUuid(), id.account.getUuid())
386 && Objects.equal(with, id.with)
387 && Objects.equal(sessionId, id.sessionId);
388 }
389
390 @Override
391 public int hashCode() {
392 return Objects.hashCode(account.getUuid(), with, sessionId);
393 }
394
395 public Account getAccount() {
396 return account;
397 }
398
399 public Jid getWith() {
400 return with;
401 }
402
403 public String getSessionId() {
404 return sessionId;
405 }
406
407 @Override
408 @NonNull
409 public String toString() {
410 return MoreObjects.toStringHelper(this)
411 .add("account", account.getJid())
412 .add("with", with)
413 .add("sessionId", sessionId)
414 .toString();
415 }
416 }
417
418 protected static State reasonToState(Reason reason) {
419 return switch (reason) {
420 case SUCCESS -> State.TERMINATED_SUCCESS;
421 case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
422 case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
423 case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
424 case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS ->
425 State.TERMINATED_APPLICATION_FAILURE;
426 default -> State.TERMINATED_CONNECTIVITY_ERROR;
427 };
428 }
429
430 public enum State {
431 NULL, // default value; nothing has been sent or received yet
432 PROPOSED,
433 ACCEPTED,
434 PROCEED,
435 REJECTED,
436 REJECTED_RACED, // used when we want to reject but haven’t received session init yet
437 RETRACTED,
438 RETRACTED_RACED, // used when receiving a retract after we already asked to proceed
439 SESSION_INITIALIZED, // equal to 'PENDING'
440 SESSION_INITIALIZED_PRE_APPROVED,
441 SESSION_ACCEPTED, // equal to 'ACTIVE'
442 TERMINATED_SUCCESS, // equal to 'ENDED' (after successful call) ui will just close
443 TERMINATED_DECLINED_OR_BUSY, // equal to 'ENDED' (after other party declined the call)
444 TERMINATED_CONNECTIVITY_ERROR, // equal to 'ENDED' (but after network failures; ui will
445 // display retry button)
446 TERMINATED_CANCEL_OR_TIMEOUT, // more or less the same as retracted; caller pressed end call
447 // before session was accepted
448 TERMINATED_APPLICATION_FAILURE,
449 TERMINATED_SECURITY_ERROR
450 }
451}