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(String.format("Unable to call finish from %s", this.state));
223 }
224 }
225
226 protected abstract void terminateTransport();
227
228 abstract void notifyRebound();
229
230 protected void sendSessionTerminate(
231 final Reason reason, final String text, final Consumer<State> trigger) {
232 final State previous = this.state;
233 final State target = reasonToState(reason);
234 transitionOrThrow(target);
235 if (previous != State.NULL && trigger != null) {
236 trigger.accept(target);
237 }
238 final JinglePacket jinglePacket =
239 new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
240 jinglePacket.setReason(reason, text);
241 send(jinglePacket);
242 finish();
243 }
244
245 protected void send(final JinglePacket jinglePacket) {
246 jinglePacket.setTo(id.with);
247 xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
248 }
249
250 protected void respondOk(final JinglePacket jinglePacket) {
251 xmppConnectionService.sendIqPacket(
252 id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
253 }
254
255 protected void respondWithTieBreak(final JinglePacket jinglePacket) {
256 respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
257 }
258
259 protected void respondWithOutOfOrder(final JinglePacket jinglePacket) {
260 respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
261 }
262
263 protected void respondWithItemNotFound(final JinglePacket jinglePacket) {
264 respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
265 }
266
267 private void respondWithJingleError(
268 final IqPacket original,
269 String jingleCondition,
270 String condition,
271 String conditionType) {
272 jingleConnectionManager.respondWithJingleError(
273 id.account, original, jingleCondition, condition, conditionType);
274 }
275
276 private synchronized void handleIqResponse(final Account account, final IqPacket response) {
277 if (response.getType() == IqPacket.TYPE.ERROR) {
278 handleIqErrorResponse(response);
279 return;
280 }
281 if (response.getType() == IqPacket.TYPE.TIMEOUT) {
282 handleIqTimeoutResponse(response);
283 }
284 }
285
286 protected void handleIqErrorResponse(final IqPacket response) {
287 Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
288 final String errorCondition = response.getErrorCondition();
289 Log.d(
290 Config.LOGTAG,
291 id.account.getJid().asBareJid()
292 + ": received IQ-error from "
293 + response.getFrom()
294 + " in RTP session. "
295 + errorCondition);
296 if (isTerminated()) {
297 Log.i(
298 Config.LOGTAG,
299 id.account.getJid().asBareJid()
300 + ": ignoring error because session was already terminated");
301 return;
302 }
303 this.terminateTransport();
304 final State target;
305 if (Arrays.asList(
306 "service-unavailable",
307 "recipient-unavailable",
308 "remote-server-not-found",
309 "remote-server-timeout")
310 .contains(errorCondition)) {
311 target = State.TERMINATED_CONNECTIVITY_ERROR;
312 } else {
313 target = State.TERMINATED_APPLICATION_FAILURE;
314 }
315 transitionOrThrow(target);
316 this.finish();
317 }
318
319 protected void handleIqTimeoutResponse(final IqPacket response) {
320 Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
321 Log.d(
322 Config.LOGTAG,
323 id.account.getJid().asBareJid()
324 + ": received IQ timeout in RTP session with "
325 + id.with
326 + ". terminating with connectivity error");
327 if (isTerminated()) {
328 Log.i(
329 Config.LOGTAG,
330 id.account.getJid().asBareJid()
331 + ": ignoring error because session was already terminated");
332 return;
333 }
334 this.terminateTransport();
335 transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
336 this.finish();
337 }
338
339 protected boolean remoteHasFeature(final String feature) {
340 final Contact contact = id.getContact();
341 final Presence presence =
342 contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
343 final ServiceDiscoveryResult serviceDiscoveryResult =
344 presence == null ? null : presence.getServiceDiscoveryResult();
345 final List<String> features =
346 serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
347 return features != null && features.contains(feature);
348 }
349
350 public static class Id {
351 public final Account account;
352 public final Jid with;
353 public final String sessionId;
354
355 private Id(final Account account, final Jid with, final String sessionId) {
356 Preconditions.checkNotNull(account);
357 Preconditions.checkNotNull(with);
358 Preconditions.checkNotNull(sessionId);
359 this.account = account;
360 this.with = with;
361 this.sessionId = sessionId;
362 }
363
364 public static Id of(Account account, JinglePacket jinglePacket) {
365 return new Id(account, jinglePacket.getFrom(), jinglePacket.getSessionId());
366 }
367
368 public static Id of(Account account, Jid with, final String sessionId) {
369 return new Id(account, with, sessionId);
370 }
371
372 public static Id of(Account account, Jid with) {
373 return new Id(account, with, JingleConnectionManager.nextRandomId());
374 }
375
376 public static Id of(Message message) {
377 return new Id(
378 message.getConversation().getAccount(),
379 message.getCounterpart(),
380 JingleConnectionManager.nextRandomId());
381 }
382
383 public Contact getContact() {
384 return account.getRoster().getContact(with);
385 }
386
387 @Override
388 public boolean equals(Object o) {
389 if (this == o) return true;
390 if (o == null || getClass() != o.getClass()) return false;
391 Id id = (Id) o;
392 return Objects.equal(account.getUuid(), id.account.getUuid())
393 && Objects.equal(with, id.with)
394 && Objects.equal(sessionId, id.sessionId);
395 }
396
397 @Override
398 public int hashCode() {
399 return Objects.hashCode(account.getUuid(), with, sessionId);
400 }
401
402 public Account getAccount() {
403 return account;
404 }
405
406 public Jid getWith() {
407 return with;
408 }
409
410 public String getSessionId() {
411 return sessionId;
412 }
413
414 @Override
415 @NonNull
416 public String toString() {
417 return MoreObjects.toStringHelper(this)
418 .add("account", account.getJid())
419 .add("with", with)
420 .add("sessionId", sessionId)
421 .toString();
422 }
423 }
424
425 protected static State reasonToState(Reason reason) {
426 return switch (reason) {
427 case SUCCESS -> State.TERMINATED_SUCCESS;
428 case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
429 case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
430 case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
431 case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
432 .TERMINATED_APPLICATION_FAILURE;
433 default -> State.TERMINATED_CONNECTIVITY_ERROR;
434 };
435 }
436
437 public enum State {
438 NULL, // default value; nothing has been sent or received yet
439 PROPOSED,
440 ACCEPTED,
441 PROCEED,
442 REJECTED,
443 REJECTED_RACED, // used when we want to reject but haven’t received session init yet
444 RETRACTED,
445 RETRACTED_RACED, // used when receiving a retract after we already asked to proceed
446 SESSION_INITIALIZED, // equal to 'PENDING'
447 SESSION_INITIALIZED_PRE_APPROVED,
448 SESSION_ACCEPTED, // equal to 'ACTIVE'
449 TERMINATED_SUCCESS, // equal to 'ENDED' (after successful call) ui will just close
450 TERMINATED_DECLINED_OR_BUSY, // equal to 'ENDED' (after other party declined the call)
451 TERMINATED_CONNECTIVITY_ERROR, // equal to 'ENDED' (but after network failures; ui will
452 // display retry button)
453 TERMINATED_CANCEL_OR_TIMEOUT, // more or less the same as retracted; caller pressed end call
454 // before session was accepted
455 TERMINATED_APPLICATION_FAILURE,
456 TERMINATED_SECURITY_ERROR
457 }
458}