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