1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Log;
4
5import androidx.annotation.NonNull;
6import androidx.annotation.Nullable;
7
8import com.google.common.base.Optional;
9import com.google.common.base.Preconditions;
10import com.google.common.base.Predicates;
11import com.google.common.base.Stopwatch;
12import com.google.common.base.Strings;
13import com.google.common.base.Throwables;
14import com.google.common.collect.Collections2;
15import com.google.common.collect.ImmutableList;
16import com.google.common.collect.ImmutableMap;
17import com.google.common.collect.Maps;
18import com.google.common.collect.Sets;
19import com.google.common.primitives.Ints;
20import com.google.common.util.concurrent.FutureCallback;
21import com.google.common.util.concurrent.Futures;
22import com.google.common.util.concurrent.ListenableFuture;
23import com.google.common.util.concurrent.MoreExecutors;
24
25import org.webrtc.EglBase;
26import org.webrtc.IceCandidate;
27import org.webrtc.PeerConnection;
28import org.webrtc.VideoTrack;
29
30import java.util.Arrays;
31import java.util.Collection;
32import java.util.Collections;
33import java.util.LinkedList;
34import java.util.List;
35import java.util.Map;
36import java.util.Queue;
37import java.util.Set;
38import java.util.concurrent.ExecutionException;
39import java.util.concurrent.ScheduledFuture;
40import java.util.concurrent.TimeUnit;
41
42import eu.siacs.conversations.Config;
43import eu.siacs.conversations.crypto.axolotl.AxolotlService;
44import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
45import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
46import eu.siacs.conversations.entities.Account;
47import eu.siacs.conversations.entities.Conversation;
48import eu.siacs.conversations.entities.Conversational;
49import eu.siacs.conversations.entities.Message;
50import eu.siacs.conversations.entities.RtpSessionStatus;
51import eu.siacs.conversations.services.AppRTCAudioManager;
52import eu.siacs.conversations.utils.IP;
53import eu.siacs.conversations.xml.Element;
54import eu.siacs.conversations.xml.Namespace;
55import eu.siacs.conversations.xmpp.Jid;
56import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
57import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
58import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
59import eu.siacs.conversations.xmpp.jingle.stanzas.Proceed;
60import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
61import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
62import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
63import eu.siacs.conversations.xmpp.stanzas.IqPacket;
64import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
65
66public class JingleRtpConnection extends AbstractJingleConnection
67 implements WebRTCWrapper.EventCallback {
68
69 public static final List<State> STATES_SHOWING_ONGOING_CALL =
70 Arrays.asList(
71 State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED);
72 private static final long BUSY_TIME_OUT = 30;
73 private static final List<State> TERMINATED =
74 Arrays.asList(
75 State.ACCEPTED,
76 State.REJECTED,
77 State.REJECTED_RACED,
78 State.RETRACTED,
79 State.RETRACTED_RACED,
80 State.TERMINATED_SUCCESS,
81 State.TERMINATED_DECLINED_OR_BUSY,
82 State.TERMINATED_CONNECTIVITY_ERROR,
83 State.TERMINATED_CANCEL_OR_TIMEOUT,
84 State.TERMINATED_APPLICATION_FAILURE,
85 State.TERMINATED_SECURITY_ERROR);
86
87 private static final Map<State, Collection<State>> VALID_TRANSITIONS;
88
89 static {
90 final ImmutableMap.Builder<State, Collection<State>> transitionBuilder =
91 new ImmutableMap.Builder<>();
92 transitionBuilder.put(
93 State.NULL,
94 ImmutableList.of(
95 State.PROPOSED,
96 State.SESSION_INITIALIZED,
97 State.TERMINATED_APPLICATION_FAILURE,
98 State.TERMINATED_SECURITY_ERROR));
99 transitionBuilder.put(
100 State.PROPOSED,
101 ImmutableList.of(
102 State.ACCEPTED,
103 State.PROCEED,
104 State.REJECTED,
105 State.RETRACTED,
106 State.TERMINATED_APPLICATION_FAILURE,
107 State.TERMINATED_SECURITY_ERROR,
108 State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection
109 // rebinds
110 ));
111 transitionBuilder.put(
112 State.PROCEED,
113 ImmutableList.of(
114 State.REJECTED_RACED,
115 State.RETRACTED_RACED,
116 State.SESSION_INITIALIZED_PRE_APPROVED,
117 State.TERMINATED_SUCCESS,
118 State.TERMINATED_APPLICATION_FAILURE,
119 State.TERMINATED_SECURITY_ERROR,
120 State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error
121 // bounces of the proceed message
122 ));
123 transitionBuilder.put(
124 State.SESSION_INITIALIZED,
125 ImmutableList.of(
126 State.SESSION_ACCEPTED,
127 State.TERMINATED_SUCCESS,
128 State.TERMINATED_DECLINED_OR_BUSY,
129 State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
130 // and IQ timeouts
131 State.TERMINATED_CANCEL_OR_TIMEOUT,
132 State.TERMINATED_APPLICATION_FAILURE,
133 State.TERMINATED_SECURITY_ERROR));
134 transitionBuilder.put(
135 State.SESSION_INITIALIZED_PRE_APPROVED,
136 ImmutableList.of(
137 State.SESSION_ACCEPTED,
138 State.TERMINATED_SUCCESS,
139 State.TERMINATED_DECLINED_OR_BUSY,
140 State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
141 // and IQ timeouts
142 State.TERMINATED_CANCEL_OR_TIMEOUT,
143 State.TERMINATED_APPLICATION_FAILURE,
144 State.TERMINATED_SECURITY_ERROR));
145 transitionBuilder.put(
146 State.SESSION_ACCEPTED,
147 ImmutableList.of(
148 State.TERMINATED_SUCCESS,
149 State.TERMINATED_DECLINED_OR_BUSY,
150 State.TERMINATED_CONNECTIVITY_ERROR,
151 State.TERMINATED_CANCEL_OR_TIMEOUT,
152 State.TERMINATED_APPLICATION_FAILURE,
153 State.TERMINATED_SECURITY_ERROR));
154 VALID_TRANSITIONS = transitionBuilder.build();
155 }
156
157 private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
158 private final Queue<Map.Entry<String, RtpContentMap.DescriptionTransport>>
159 pendingIceCandidates = new LinkedList<>();
160 private final OmemoVerification omemoVerification = new OmemoVerification();
161 private final Message message;
162 private State state = State.NULL;
163 private Set<Media> proposedMedia;
164 private RtpContentMap initiatorRtpContentMap;
165 private RtpContentMap responderRtpContentMap;
166 private IceUdpTransportInfo.Setup peerDtlsSetup;
167 private final Stopwatch sessionDuration = Stopwatch.createUnstarted();
168 private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
169 private ScheduledFuture<?> ringingTimeoutFuture;
170
171 JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
172 super(jingleConnectionManager, id, initiator);
173 final Conversation conversation =
174 jingleConnectionManager
175 .getXmppConnectionService()
176 .findOrCreateConversation(id.account, id.with.asBareJid(), false, false);
177 this.message =
178 new Message(
179 conversation,
180 isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED,
181 Message.TYPE_RTP_SESSION,
182 id.sessionId);
183 }
184
185 private static State reasonToState(Reason reason) {
186 switch (reason) {
187 case SUCCESS:
188 return State.TERMINATED_SUCCESS;
189 case DECLINE:
190 case BUSY:
191 return State.TERMINATED_DECLINED_OR_BUSY;
192 case CANCEL:
193 case TIMEOUT:
194 return State.TERMINATED_CANCEL_OR_TIMEOUT;
195 case SECURITY_ERROR:
196 return State.TERMINATED_SECURITY_ERROR;
197 case FAILED_APPLICATION:
198 case UNSUPPORTED_TRANSPORTS:
199 case UNSUPPORTED_APPLICATIONS:
200 return State.TERMINATED_APPLICATION_FAILURE;
201 default:
202 return State.TERMINATED_CONNECTIVITY_ERROR;
203 }
204 }
205
206 @Override
207 synchronized void deliverPacket(final JinglePacket jinglePacket) {
208 switch (jinglePacket.getAction()) {
209 case SESSION_INITIATE:
210 receiveSessionInitiate(jinglePacket);
211 break;
212 case TRANSPORT_INFO:
213 receiveTransportInfo(jinglePacket);
214 break;
215 case SESSION_ACCEPT:
216 receiveSessionAccept(jinglePacket);
217 break;
218 case SESSION_TERMINATE:
219 receiveSessionTerminate(jinglePacket);
220 break;
221 default:
222 respondOk(jinglePacket);
223 Log.d(
224 Config.LOGTAG,
225 String.format(
226 "%s: received unhandled jingle action %s",
227 id.account.getJid().asBareJid(), jinglePacket.getAction()));
228 break;
229 }
230 }
231
232 @Override
233 synchronized void notifyRebound() {
234 if (isTerminated()) {
235 return;
236 }
237 webRTCWrapper.close();
238 if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) {
239 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
240 }
241 if (isInState(
242 State.SESSION_INITIALIZED,
243 State.SESSION_INITIALIZED_PRE_APPROVED,
244 State.SESSION_ACCEPTED)) {
245 // we might have already changed resources (full jid) at this point; so this might not
246 // even reach the other party
247 sendSessionTerminate(Reason.CONNECTIVITY_ERROR);
248 } else {
249 transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
250 finish();
251 }
252 }
253
254 private void receiveSessionTerminate(final JinglePacket jinglePacket) {
255 respondOk(jinglePacket);
256 final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason();
257 final State previous = this.state;
258 Log.d(
259 Config.LOGTAG,
260 id.account.getJid().asBareJid()
261 + ": received session terminate reason="
262 + wrapper.reason
263 + "("
264 + Strings.nullToEmpty(wrapper.text)
265 + ") while in state "
266 + previous);
267 if (TERMINATED.contains(previous)) {
268 Log.d(
269 Config.LOGTAG,
270 id.account.getJid().asBareJid()
271 + ": ignoring session terminate because already in "
272 + previous);
273 return;
274 }
275 webRTCWrapper.close();
276 final State target = reasonToState(wrapper.reason);
277 transitionOrThrow(target);
278 writeLogMessage(target);
279 if (previous == State.PROPOSED || previous == State.SESSION_INITIALIZED) {
280 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
281 }
282 finish();
283 }
284
285 private void receiveTransportInfo(final JinglePacket jinglePacket) {
286 // Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to
287 // INITIALIZED only after transport-info has been received
288 if (isInState(
289 State.NULL,
290 State.PROCEED,
291 State.SESSION_INITIALIZED,
292 State.SESSION_INITIALIZED_PRE_APPROVED,
293 State.SESSION_ACCEPTED)) {
294 final RtpContentMap contentMap;
295 try {
296 contentMap = RtpContentMap.of(jinglePacket);
297 } catch (final IllegalArgumentException | NullPointerException e) {
298 Log.d(
299 Config.LOGTAG,
300 id.account.getJid().asBareJid()
301 + ": improperly formatted contents; ignoring",
302 e);
303 respondOk(jinglePacket);
304 return;
305 }
306 receiveTransportInfo(jinglePacket, contentMap);
307 } else {
308 if (isTerminated()) {
309 respondOk(jinglePacket);
310 Log.d(
311 Config.LOGTAG,
312 id.account.getJid().asBareJid()
313 + ": ignoring out-of-order transport info; we where already terminated");
314 } else {
315 Log.d(
316 Config.LOGTAG,
317 id.account.getJid().asBareJid()
318 + ": received transport info while in state="
319 + this.state);
320 terminateWithOutOfOrder(jinglePacket);
321 }
322 }
323 }
324
325 private void receiveTransportInfo(
326 final JinglePacket jinglePacket, final RtpContentMap contentMap) {
327 final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates =
328 contentMap.contents.entrySet();
329 if (this.state == State.SESSION_ACCEPTED) {
330 // zero candidates + modified credentials are an ICE restart offer
331 if (checkForIceRestart(jinglePacket, contentMap)) {
332 return;
333 }
334 respondOk(jinglePacket);
335 try {
336 processCandidates(candidates);
337 } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
338 Log.w(
339 Config.LOGTAG,
340 id.account.getJid().asBareJid()
341 + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored");
342 }
343 } else {
344 respondOk(jinglePacket);
345 pendingIceCandidates.addAll(candidates);
346 }
347 }
348
349 private boolean checkForIceRestart(
350 final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) {
351 final RtpContentMap existing = getRemoteContentMap();
352 final Set<IceUdpTransportInfo.Credentials> existingCredentials;
353 final IceUdpTransportInfo.Credentials newCredentials;
354 try {
355 existingCredentials = existing.getCredentials();
356 newCredentials = rtpContentMap.getDistinctCredentials();
357 } catch (final IllegalStateException e) {
358 Log.d(Config.LOGTAG, "unable to gather credentials for comparison", e);
359 return false;
360 }
361 if (existingCredentials.contains(newCredentials)) {
362 return false;
363 }
364 // TODO an alternative approach is to check if we already got an iq result to our
365 // ICE-restart
366 // and if that's the case we are seeing an answer.
367 // This might be more spec compliant but also more error prone potentially
368 final boolean isOffer = rtpContentMap.emptyCandidates();
369 final RtpContentMap restartContentMap;
370 try {
371 if (isOffer) {
372 Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials);
373 restartContentMap =
374 existing.modifiedCredentials(
375 newCredentials, IceUdpTransportInfo.Setup.ACTPASS);
376 } else {
377 final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup();
378 Log.d(
379 Config.LOGTAG,
380 "received confirmation of ICE restart"
381 + newCredentials
382 + " peer_setup="
383 + setup);
384 // DTLS setup attribute needs to be rewritten to reflect current peer state
385 // https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM
386 restartContentMap = existing.modifiedCredentials(newCredentials, setup);
387 }
388 if (applyIceRestart(jinglePacket, restartContentMap, isOffer)) {
389 return isOffer;
390 } else {
391 Log.d(Config.LOGTAG, "ignoring ICE restart. sending tie-break");
392 respondWithTieBreak(jinglePacket);
393 return true;
394 }
395 } catch (final Exception exception) {
396 respondOk(jinglePacket);
397 final Throwable rootCause = Throwables.getRootCause(exception);
398 if (rootCause instanceof WebRTCWrapper.PeerConnectionNotInitialized) {
399 // If this happens a termination is already in progress
400 Log.d(Config.LOGTAG, "ignoring PeerConnectionNotInitialized on ICE restart");
401 return true;
402 }
403 Log.d(Config.LOGTAG, "failure to apply ICE restart", rootCause);
404 webRTCWrapper.close();
405 sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
406 return true;
407 }
408 }
409
410 private IceUdpTransportInfo.Setup getPeerDtlsSetup() {
411 final IceUdpTransportInfo.Setup peerSetup = this.peerDtlsSetup;
412 if (peerSetup == null || peerSetup == IceUdpTransportInfo.Setup.ACTPASS) {
413 throw new IllegalStateException("Invalid peer setup");
414 }
415 return peerSetup;
416 }
417
418 private void storePeerDtlsSetup(final IceUdpTransportInfo.Setup setup) {
419 if (setup == null || setup == IceUdpTransportInfo.Setup.ACTPASS) {
420 throw new IllegalArgumentException("Trying to store invalid peer dtls setup");
421 }
422 this.peerDtlsSetup = setup;
423 }
424
425 private boolean applyIceRestart(
426 final JinglePacket jinglePacket,
427 final RtpContentMap restartContentMap,
428 final boolean isOffer)
429 throws ExecutionException, InterruptedException {
430 final SessionDescription sessionDescription = SessionDescription.of(restartContentMap, !isInitiator());
431 final org.webrtc.SessionDescription.Type type =
432 isOffer
433 ? org.webrtc.SessionDescription.Type.OFFER
434 : org.webrtc.SessionDescription.Type.ANSWER;
435 org.webrtc.SessionDescription sdp =
436 new org.webrtc.SessionDescription(type, sessionDescription.toString());
437 if (isOffer && webRTCWrapper.getSignalingState() != PeerConnection.SignalingState.STABLE) {
438 if (isInitiator()) {
439 // We ignore the offer and respond with tie-break. This will clause the responder
440 // not to apply the content map
441 return false;
442 }
443 }
444 webRTCWrapper.setRemoteDescription(sdp).get();
445 setRemoteContentMap(restartContentMap);
446 if (isOffer) {
447 webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
448 final SessionDescription localSessionDescription = setLocalSessionDescription();
449 setLocalContentMap(RtpContentMap.of(localSessionDescription, isInitiator()));
450 // We need to respond OK before sending any candidates
451 respondOk(jinglePacket);
452 webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
453 } else {
454 storePeerDtlsSetup(restartContentMap.getDtlsSetup());
455 }
456 return true;
457 }
458
459 private void processCandidates(
460 final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
461 for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) {
462 processCandidate(content);
463 }
464 }
465
466 private void processCandidate(
467 final Map.Entry<String, RtpContentMap.DescriptionTransport> content) {
468 final RtpContentMap rtpContentMap = getRemoteContentMap();
469 final List<String> indices = toIdentificationTags(rtpContentMap);
470 final String sdpMid = content.getKey(); // aka content name
471 final IceUdpTransportInfo transport = content.getValue().transport;
472 final IceUdpTransportInfo.Credentials credentials = transport.getCredentials();
473
474 // TODO check that credentials remained the same
475
476 for (final IceUdpTransportInfo.Candidate candidate : transport.getCandidates()) {
477 final String sdp;
478 try {
479 sdp = candidate.toSdpAttribute(credentials.ufrag);
480 } catch (final IllegalArgumentException e) {
481 Log.d(
482 Config.LOGTAG,
483 id.account.getJid().asBareJid()
484 + ": ignoring invalid ICE candidate "
485 + e.getMessage());
486 continue;
487 }
488 final int mLineIndex = indices.indexOf(sdpMid);
489 if (mLineIndex < 0) {
490 Log.w(
491 Config.LOGTAG,
492 "mLineIndex not found for " + sdpMid + ". available indices " + indices);
493 }
494 final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp);
495 Log.d(Config.LOGTAG, "received candidate: " + iceCandidate);
496 this.webRTCWrapper.addIceCandidate(iceCandidate);
497 }
498 }
499
500 private RtpContentMap getRemoteContentMap() {
501 return isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap;
502 }
503
504 private RtpContentMap getLocalContentMap() {
505 return isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
506 }
507
508 private List<String> toIdentificationTags(final RtpContentMap rtpContentMap) {
509 final Group originalGroup = rtpContentMap.group;
510 final List<String> identificationTags =
511 originalGroup == null
512 ? rtpContentMap.getNames()
513 : originalGroup.getIdentificationTags();
514 if (identificationTags.size() == 0) {
515 Log.w(
516 Config.LOGTAG,
517 id.account.getJid().asBareJid()
518 + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices");
519 }
520 return identificationTags;
521 }
522
523 private ListenableFuture<RtpContentMap> receiveRtpContentMap(
524 final JinglePacket jinglePacket, final boolean expectVerification) {
525 final RtpContentMap receivedContentMap;
526 try {
527 receivedContentMap = RtpContentMap.of(jinglePacket);
528 } catch (final Exception e) {
529 return Futures.immediateFailedFuture(e);
530 }
531 if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) {
532 final ListenableFuture<AxolotlService.OmemoVerifiedPayload<RtpContentMap>> future =
533 id.account
534 .getAxolotlService()
535 .decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with);
536 return Futures.transform(
537 future,
538 omemoVerifiedPayload -> {
539 // TODO test if an exception here triggers a correct abort
540 omemoVerification.setOrEnsureEqual(omemoVerifiedPayload);
541 Log.d(
542 Config.LOGTAG,
543 id.account.getJid().asBareJid()
544 + ": received verifiable DTLS fingerprint via "
545 + omemoVerification);
546 return omemoVerifiedPayload.getPayload();
547 },
548 MoreExecutors.directExecutor());
549 } else if (Config.REQUIRE_RTP_VERIFICATION || expectVerification) {
550 return Futures.immediateFailedFuture(
551 new SecurityException("DTLS fingerprint was unexpectedly not verifiable"));
552 } else {
553 return Futures.immediateFuture(receivedContentMap);
554 }
555 }
556
557 private void receiveSessionInitiate(final JinglePacket jinglePacket) {
558 if (isInitiator()) {
559 Log.d(
560 Config.LOGTAG,
561 String.format(
562 "%s: received session-initiate even though we were initiating",
563 id.account.getJid().asBareJid()));
564 if (isTerminated()) {
565 Log.d(
566 Config.LOGTAG,
567 String.format(
568 "%s: got a reason to terminate with out-of-order. but already in state %s",
569 id.account.getJid().asBareJid(), getState()));
570 respondWithOutOfOrder(jinglePacket);
571 } else {
572 terminateWithOutOfOrder(jinglePacket);
573 }
574 return;
575 }
576 final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
577 Futures.addCallback(
578 future,
579 new FutureCallback<RtpContentMap>() {
580 @Override
581 public void onSuccess(@Nullable RtpContentMap rtpContentMap) {
582 receiveSessionInitiate(jinglePacket, rtpContentMap);
583 }
584
585 @Override
586 public void onFailure(@NonNull final Throwable throwable) {
587 respondOk(jinglePacket);
588 sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage());
589 }
590 },
591 MoreExecutors.directExecutor());
592 }
593
594 private void receiveSessionInitiate(
595 final JinglePacket jinglePacket, final RtpContentMap contentMap) {
596 try {
597 contentMap.requireContentDescriptions();
598 contentMap.requireDTLSFingerprint(true);
599 } catch (final RuntimeException e) {
600 Log.d(
601 Config.LOGTAG,
602 id.account.getJid().asBareJid() + ": improperly formatted contents",
603 Throwables.getRootCause(e));
604 respondOk(jinglePacket);
605 sendSessionTerminate(Reason.of(e), e.getMessage());
606 return;
607 }
608 Log.d(
609 Config.LOGTAG,
610 "processing session-init with " + contentMap.contents.size() + " contents");
611 final State target;
612 if (this.state == State.PROCEED) {
613 Preconditions.checkState(
614 proposedMedia != null && proposedMedia.size() > 0,
615 "proposed media must be set when processing pre-approved session-initiate");
616 if (!this.proposedMedia.equals(contentMap.getMedia())) {
617 sendSessionTerminate(
618 Reason.SECURITY_ERROR,
619 String.format(
620 "Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s",
621 this.proposedMedia, contentMap.getMedia()));
622 return;
623 }
624 target = State.SESSION_INITIALIZED_PRE_APPROVED;
625 } else {
626 target = State.SESSION_INITIALIZED;
627 }
628 if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
629 respondOk(jinglePacket);
630 pendingIceCandidates.addAll(contentMap.contents.entrySet());
631 if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
632 Log.d(
633 Config.LOGTAG,
634 id.account.getJid().asBareJid()
635 + ": automatically accepting session-initiate");
636 sendSessionAccept();
637 } else {
638 Log.d(
639 Config.LOGTAG,
640 id.account.getJid().asBareJid()
641 + ": received not pre-approved session-initiate. start ringing");
642 startRinging();
643 }
644 } else {
645 Log.d(
646 Config.LOGTAG,
647 String.format(
648 "%s: received session-initiate while in state %s",
649 id.account.getJid().asBareJid(), state));
650 terminateWithOutOfOrder(jinglePacket);
651 }
652 }
653
654 private void receiveSessionAccept(final JinglePacket jinglePacket) {
655 if (!isInitiator()) {
656 Log.d(
657 Config.LOGTAG,
658 String.format(
659 "%s: received session-accept even though we were responding",
660 id.account.getJid().asBareJid()));
661 terminateWithOutOfOrder(jinglePacket);
662 return;
663 }
664 final ListenableFuture<RtpContentMap> future =
665 receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint());
666 Futures.addCallback(
667 future,
668 new FutureCallback<RtpContentMap>() {
669 @Override
670 public void onSuccess(@Nullable RtpContentMap rtpContentMap) {
671 receiveSessionAccept(jinglePacket, rtpContentMap);
672 }
673
674 @Override
675 public void onFailure(@NonNull final Throwable throwable) {
676 respondOk(jinglePacket);
677 Log.d(
678 Config.LOGTAG,
679 id.account.getJid().asBareJid()
680 + ": improperly formatted contents in session-accept",
681 throwable);
682 webRTCWrapper.close();
683 sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage());
684 }
685 },
686 MoreExecutors.directExecutor());
687 }
688
689 private void receiveSessionAccept(
690 final JinglePacket jinglePacket, final RtpContentMap contentMap) {
691 try {
692 contentMap.requireContentDescriptions();
693 contentMap.requireDTLSFingerprint();
694 } catch (final RuntimeException e) {
695 respondOk(jinglePacket);
696 Log.d(
697 Config.LOGTAG,
698 id.account.getJid().asBareJid()
699 + ": improperly formatted contents in session-accept",
700 e);
701 webRTCWrapper.close();
702 sendSessionTerminate(Reason.of(e), e.getMessage());
703 return;
704 }
705 final Set<Media> initiatorMedia = this.initiatorRtpContentMap.getMedia();
706 if (!initiatorMedia.equals(contentMap.getMedia())) {
707 sendSessionTerminate(
708 Reason.SECURITY_ERROR,
709 String.format(
710 "Your session-included included media %s but our session-initiate was %s",
711 this.proposedMedia, contentMap.getMedia()));
712 return;
713 }
714 Log.d(
715 Config.LOGTAG,
716 "processing session-accept with " + contentMap.contents.size() + " contents");
717 if (transition(State.SESSION_ACCEPTED)) {
718 respondOk(jinglePacket);
719 receiveSessionAccept(contentMap);
720 } else {
721 Log.d(
722 Config.LOGTAG,
723 String.format(
724 "%s: received session-accept while in state %s",
725 id.account.getJid().asBareJid(), state));
726 respondOk(jinglePacket);
727 }
728 }
729
730 private void receiveSessionAccept(final RtpContentMap contentMap) {
731 this.responderRtpContentMap = contentMap;
732 this.storePeerDtlsSetup(contentMap.getDtlsSetup());
733 final SessionDescription sessionDescription;
734 try {
735 sessionDescription = SessionDescription.of(contentMap, false);
736 } catch (final IllegalArgumentException | NullPointerException e) {
737 Log.d(
738 Config.LOGTAG,
739 id.account.getJid().asBareJid()
740 + ": unable convert offer from session-accept to SDP",
741 e);
742 webRTCWrapper.close();
743 sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
744 return;
745 }
746 final org.webrtc.SessionDescription answer =
747 new org.webrtc.SessionDescription(
748 org.webrtc.SessionDescription.Type.ANSWER, sessionDescription.toString());
749 try {
750 this.webRTCWrapper.setRemoteDescription(answer).get();
751 } catch (final Exception e) {
752 Log.d(
753 Config.LOGTAG,
754 id.account.getJid().asBareJid()
755 + ": unable to set remote description after receiving session-accept",
756 Throwables.getRootCause(e));
757 webRTCWrapper.close();
758 sendSessionTerminate(
759 Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage());
760 return;
761 }
762 processCandidates(contentMap.contents.entrySet());
763 }
764
765 private void sendSessionAccept() {
766 final RtpContentMap rtpContentMap = this.initiatorRtpContentMap;
767 if (rtpContentMap == null) {
768 throw new IllegalStateException("initiator RTP Content Map has not been set");
769 }
770 final SessionDescription offer;
771 try {
772 offer = SessionDescription.of(rtpContentMap, true);
773 } catch (final IllegalArgumentException | NullPointerException e) {
774 Log.d(
775 Config.LOGTAG,
776 id.account.getJid().asBareJid()
777 + ": unable convert offer from session-initiate to SDP",
778 e);
779 webRTCWrapper.close();
780 sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
781 return;
782 }
783 sendSessionAccept(rtpContentMap.getMedia(), offer);
784 }
785
786 private void sendSessionAccept(final Set<Media> media, final SessionDescription offer) {
787 discoverIceServers(iceServers -> sendSessionAccept(media, offer, iceServers));
788 }
789
790 private synchronized void sendSessionAccept(
791 final Set<Media> media,
792 final SessionDescription offer,
793 final List<PeerConnection.IceServer> iceServers) {
794 if (isTerminated()) {
795 Log.w(
796 Config.LOGTAG,
797 id.account.getJid().asBareJid()
798 + ": ICE servers got discovered when session was already terminated. nothing to do.");
799 return;
800 }
801 try {
802 setupWebRTC(media, iceServers);
803 } catch (final WebRTCWrapper.InitializationException e) {
804 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC");
805 webRTCWrapper.close();
806 sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
807 return;
808 }
809 final org.webrtc.SessionDescription sdp =
810 new org.webrtc.SessionDescription(
811 org.webrtc.SessionDescription.Type.OFFER, offer.toString());
812 try {
813 this.webRTCWrapper.setRemoteDescription(sdp).get();
814 addIceCandidatesFromBlackLog();
815 org.webrtc.SessionDescription webRTCSessionDescription =
816 this.webRTCWrapper.setLocalDescription().get();
817 prepareSessionAccept(webRTCSessionDescription);
818 } catch (final Exception e) {
819 failureToAcceptSession(e);
820 }
821 }
822
823 private void failureToAcceptSession(final Throwable throwable) {
824 if (isTerminated()) {
825 return;
826 }
827 final Throwable rootCause = Throwables.getRootCause(throwable);
828 Log.d(Config.LOGTAG, "unable to send session accept", rootCause);
829 webRTCWrapper.close();
830 sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
831 }
832
833 private void addIceCandidatesFromBlackLog() {
834 Map.Entry<String, RtpContentMap.DescriptionTransport> foo;
835 while ((foo = this.pendingIceCandidates.poll()) != null) {
836 processCandidate(foo);
837 Log.d(
838 Config.LOGTAG,
839 id.account.getJid().asBareJid() + ": added candidate from back log");
840 }
841 }
842
843 private void prepareSessionAccept(
844 final org.webrtc.SessionDescription webRTCSessionDescription) {
845 final SessionDescription sessionDescription =
846 SessionDescription.parse(webRTCSessionDescription.description);
847 final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false);
848 this.responderRtpContentMap = respondingRtpContentMap;
849 storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip());
850 webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
851 final ListenableFuture<RtpContentMap> outgoingContentMapFuture =
852 prepareOutgoingContentMap(respondingRtpContentMap);
853 Futures.addCallback(
854 outgoingContentMapFuture,
855 new FutureCallback<RtpContentMap>() {
856 @Override
857 public void onSuccess(final RtpContentMap outgoingContentMap) {
858 sendSessionAccept(outgoingContentMap);
859 }
860
861 @Override
862 public void onFailure(@NonNull Throwable throwable) {
863 failureToAcceptSession(throwable);
864 }
865 },
866 MoreExecutors.directExecutor());
867 }
868
869 private void sendSessionAccept(final RtpContentMap rtpContentMap) {
870 if (isTerminated()) {
871 Log.w(
872 Config.LOGTAG,
873 id.account.getJid().asBareJid()
874 + ": preparing session accept was too slow. already terminated. nothing to do.");
875 return;
876 }
877 transitionOrThrow(State.SESSION_ACCEPTED);
878 final JinglePacket sessionAccept =
879 rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
880 send(sessionAccept);
881 }
882
883 private ListenableFuture<RtpContentMap> prepareOutgoingContentMap(
884 final RtpContentMap rtpContentMap) {
885 if (this.omemoVerification.hasDeviceId()) {
886 ListenableFuture<AxolotlService.OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>>
887 verifiedPayloadFuture =
888 id.account
889 .getAxolotlService()
890 .encrypt(
891 rtpContentMap,
892 id.with,
893 omemoVerification.getDeviceId());
894 return Futures.transform(
895 verifiedPayloadFuture,
896 verifiedPayload -> {
897 omemoVerification.setOrEnsureEqual(verifiedPayload);
898 return verifiedPayload.getPayload();
899 },
900 MoreExecutors.directExecutor());
901 } else {
902 return Futures.immediateFuture(rtpContentMap);
903 }
904 }
905
906 synchronized void deliveryMessage(
907 final Jid from,
908 final Element message,
909 final String serverMessageId,
910 final long timestamp) {
911 Log.d(
912 Config.LOGTAG,
913 id.account.getJid().asBareJid()
914 + ": delivered message to JingleRtpConnection "
915 + message);
916 switch (message.getName()) {
917 case "propose":
918 receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp);
919 break;
920 case "proceed":
921 receiveProceed(from, Proceed.upgrade(message), serverMessageId, timestamp);
922 break;
923 case "retract":
924 receiveRetract(from, serverMessageId, timestamp);
925 break;
926 case "reject":
927 receiveReject(from, serverMessageId, timestamp);
928 break;
929 case "accept":
930 receiveAccept(from, serverMessageId, timestamp);
931 break;
932 default:
933 break;
934 }
935 }
936
937 void deliverFailedProceed(final String message) {
938 Log.d(
939 Config.LOGTAG,
940 id.account.getJid().asBareJid() + ": receive message error for proceed message ("+Strings.nullToEmpty(message)+")");
941 if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) {
942 webRTCWrapper.close();
943 Log.d(
944 Config.LOGTAG,
945 id.account.getJid().asBareJid() + ": transitioned into connectivity error");
946 this.finish();
947 }
948 }
949
950 private void receiveAccept(final Jid from, final String serverMsgId, final long timestamp) {
951 final boolean originatedFromMyself =
952 from.asBareJid().equals(id.account.getJid().asBareJid());
953 if (originatedFromMyself) {
954 if (transition(State.ACCEPTED)) {
955 acceptedOnOtherDevice(serverMsgId, timestamp);
956 } else {
957 Log.d(
958 Config.LOGTAG,
959 id.account.getJid().asBareJid()
960 + ": unable to transition to accept because already in state="
961 + this.state);
962 }
963 } else {
964 Log.d(
965 Config.LOGTAG,
966 id.account.getJid().asBareJid() + ": ignoring 'accept' from " + from);
967 }
968 }
969
970 private void acceptedOnOtherDevice(final String serverMsgId, final long timestamp) {
971 if (serverMsgId != null) {
972 this.message.setServerMsgId(serverMsgId);
973 }
974 this.message.setTime(timestamp);
975 this.message.setCarbon(true); // indicate that call was accepted on other device
976 this.writeLogMessageSuccess(0);
977 this.xmppConnectionService
978 .getNotificationService()
979 .cancelIncomingCallNotification();
980 this.finish();
981 }
982
983 private void receiveReject(final Jid from, final String serverMsgId, final long timestamp) {
984 final boolean originatedFromMyself =
985 from.asBareJid().equals(id.account.getJid().asBareJid());
986 // reject from another one of my clients
987 if (originatedFromMyself) {
988 receiveRejectFromMyself(serverMsgId, timestamp);
989 } else if (isInitiator()) {
990 if (from.equals(id.with)) {
991 receiveRejectFromResponder();
992 } else {
993 Log.d(
994 Config.LOGTAG,
995 id.account.getJid()
996 + ": ignoring reject from "
997 + from
998 + " for session with "
999 + id.with);
1000 }
1001 } else {
1002 Log.d(
1003 Config.LOGTAG,
1004 id.account.getJid()
1005 + ": ignoring reject from "
1006 + from
1007 + " for session with "
1008 + id.with);
1009 }
1010 }
1011
1012 private void receiveRejectFromMyself(String serverMsgId, long timestamp) {
1013 if (transition(State.REJECTED)) {
1014 this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1015 this.finish();
1016 if (serverMsgId != null) {
1017 this.message.setServerMsgId(serverMsgId);
1018 }
1019 this.message.setTime(timestamp);
1020 this.message.setCarbon(true); // indicate that call was rejected on other device
1021 writeLogMessageMissed();
1022 } else {
1023 Log.d(
1024 Config.LOGTAG,
1025 "not able to transition into REJECTED because already in " + this.state);
1026 }
1027 }
1028
1029 private void receiveRejectFromResponder() {
1030 if (isInState(State.PROCEED)) {
1031 Log.d(
1032 Config.LOGTAG,
1033 id.account.getJid()
1034 + ": received reject while still in proceed. callee reconsidered");
1035 closeTransitionLogFinish(State.REJECTED_RACED);
1036 return;
1037 }
1038 if (isInState(State.SESSION_INITIALIZED_PRE_APPROVED)) {
1039 Log.d(
1040 Config.LOGTAG,
1041 id.account.getJid()
1042 + ": received reject while in SESSION_INITIATED_PRE_APPROVED. callee reconsidered before receiving session-init");
1043 closeTransitionLogFinish(State.TERMINATED_DECLINED_OR_BUSY);
1044 return;
1045 }
1046 Log.d(
1047 Config.LOGTAG,
1048 id.account.getJid()
1049 + ": ignoring reject from responder because already in state "
1050 + this.state);
1051 }
1052
1053 private void receivePropose(
1054 final Jid from, final Propose propose, final String serverMsgId, final long timestamp) {
1055 final boolean originatedFromMyself =
1056 from.asBareJid().equals(id.account.getJid().asBareJid());
1057 if (originatedFromMyself) {
1058 Log.d(
1059 Config.LOGTAG,
1060 id.account.getJid().asBareJid() + ": saw proposal from myself. ignoring");
1061 } else if (transition(
1062 State.PROPOSED,
1063 () -> {
1064 final Collection<RtpDescription> descriptions =
1065 Collections2.transform(
1066 Collections2.filter(
1067 propose.getDescriptions(),
1068 d -> d instanceof RtpDescription),
1069 input -> (RtpDescription) input);
1070 final Collection<Media> media =
1071 Collections2.transform(descriptions, RtpDescription::getMedia);
1072 Preconditions.checkState(
1073 !media.contains(Media.UNKNOWN),
1074 "RTP descriptions contain unknown media");
1075 Log.d(
1076 Config.LOGTAG,
1077 id.account.getJid().asBareJid()
1078 + ": received session proposal from "
1079 + from
1080 + " for "
1081 + media);
1082 this.proposedMedia = Sets.newHashSet(media);
1083 })) {
1084 if (serverMsgId != null) {
1085 this.message.setServerMsgId(serverMsgId);
1086 }
1087 this.message.setTime(timestamp);
1088 startRinging();
1089 } else {
1090 Log.d(
1091 Config.LOGTAG,
1092 id.account.getJid()
1093 + ": ignoring session proposal because already in "
1094 + state);
1095 }
1096 }
1097
1098 private void startRinging() {
1099 Log.d(
1100 Config.LOGTAG,
1101 id.account.getJid().asBareJid()
1102 + ": received call from "
1103 + id.with
1104 + ". start ringing");
1105 ringingTimeoutFuture =
1106 jingleConnectionManager.schedule(
1107 this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS);
1108 xmppConnectionService.getNotificationService().startRinging(id, getMedia());
1109 }
1110
1111 private synchronized void ringingTimeout() {
1112 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": timeout reached for ringing");
1113 switch (this.state) {
1114 case PROPOSED:
1115 message.markUnread();
1116 rejectCallFromProposed();
1117 break;
1118 case SESSION_INITIALIZED:
1119 message.markUnread();
1120 rejectCallFromSessionInitiate();
1121 break;
1122 }
1123 xmppConnectionService.getNotificationService().pushMissedCallNow(message);
1124 }
1125
1126 private void cancelRingingTimeout() {
1127 final ScheduledFuture<?> future = this.ringingTimeoutFuture;
1128 if (future != null && !future.isCancelled()) {
1129 future.cancel(false);
1130 }
1131 }
1132
1133 private void receiveProceed(
1134 final Jid from, final Proceed proceed, final String serverMsgId, final long timestamp) {
1135 final Set<Media> media =
1136 Preconditions.checkNotNull(
1137 this.proposedMedia, "Proposed media has to be set before handling proceed");
1138 Preconditions.checkState(media.size() > 0, "Proposed media should not be empty");
1139 if (from.equals(id.with)) {
1140 if (isInitiator()) {
1141 if (transition(State.PROCEED)) {
1142 if (serverMsgId != null) {
1143 this.message.setServerMsgId(serverMsgId);
1144 }
1145 this.message.setTime(timestamp);
1146 final Integer remoteDeviceId = proceed.getDeviceId();
1147 if (isOmemoEnabled()) {
1148 this.omemoVerification.setDeviceId(remoteDeviceId);
1149 } else {
1150 if (remoteDeviceId != null) {
1151 Log.d(
1152 Config.LOGTAG,
1153 id.account.getJid().asBareJid()
1154 + ": remote party signaled support for OMEMO verification but we have OMEMO disabled");
1155 }
1156 this.omemoVerification.setDeviceId(null);
1157 }
1158 this.sendSessionInitiate(media, State.SESSION_INITIALIZED_PRE_APPROVED);
1159 } else {
1160 Log.d(
1161 Config.LOGTAG,
1162 String.format(
1163 "%s: ignoring proceed because already in %s",
1164 id.account.getJid().asBareJid(), this.state));
1165 }
1166 } else {
1167 Log.d(
1168 Config.LOGTAG,
1169 String.format(
1170 "%s: ignoring proceed because we were not initializing",
1171 id.account.getJid().asBareJid()));
1172 }
1173 } else if (from.asBareJid().equals(id.account.getJid().asBareJid())) {
1174 if (transition(State.ACCEPTED)) {
1175 Log.d(
1176 Config.LOGTAG,
1177 id.account.getJid().asBareJid()
1178 + ": moved session with "
1179 + id.with
1180 + " into state accepted after received carbon copied proceed");
1181 acceptedOnOtherDevice(serverMsgId, timestamp);
1182 }
1183 } else {
1184 Log.d(
1185 Config.LOGTAG,
1186 String.format(
1187 "%s: ignoring proceed from %s. was expected from %s",
1188 id.account.getJid().asBareJid(), from, id.with));
1189 }
1190 }
1191
1192 private void receiveRetract(final Jid from, final String serverMsgId, final long timestamp) {
1193 if (from.equals(id.with)) {
1194 final State target =
1195 this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED;
1196 if (transition(target)) {
1197 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1198 xmppConnectionService.getNotificationService().pushMissedCallNow(message);
1199 Log.d(
1200 Config.LOGTAG,
1201 id.account.getJid().asBareJid()
1202 + ": session with "
1203 + id.with
1204 + " has been retracted (serverMsgId="
1205 + serverMsgId
1206 + ")");
1207 if (serverMsgId != null) {
1208 this.message.setServerMsgId(serverMsgId);
1209 }
1210 this.message.setTime(timestamp);
1211 if (target == State.RETRACTED) {
1212 this.message.markUnread();
1213 }
1214 writeLogMessageMissed();
1215 finish();
1216 } else {
1217 Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state);
1218 }
1219 } else {
1220 // TODO parse retract from self
1221 Log.d(
1222 Config.LOGTAG,
1223 id.account.getJid().asBareJid()
1224 + ": received retract from "
1225 + from
1226 + ". expected retract from"
1227 + id.with
1228 + ". ignoring");
1229 }
1230 }
1231
1232 public void sendSessionInitiate() {
1233 sendSessionInitiate(this.proposedMedia, State.SESSION_INITIALIZED);
1234 }
1235
1236 private void sendSessionInitiate(final Set<Media> media, final State targetState) {
1237 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
1238 discoverIceServers(iceServers -> sendSessionInitiate(media, targetState, iceServers));
1239 }
1240
1241 private synchronized void sendSessionInitiate(
1242 final Set<Media> media,
1243 final State targetState,
1244 final List<PeerConnection.IceServer> iceServers) {
1245 if (isTerminated()) {
1246 Log.w(
1247 Config.LOGTAG,
1248 id.account.getJid().asBareJid()
1249 + ": ICE servers got discovered when session was already terminated. nothing to do.");
1250 return;
1251 }
1252 try {
1253 setupWebRTC(media, iceServers);
1254 } catch (final WebRTCWrapper.InitializationException e) {
1255 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC");
1256 webRTCWrapper.close();
1257 sendRetract(Reason.ofThrowable(e));
1258 return;
1259 }
1260 try {
1261 org.webrtc.SessionDescription webRTCSessionDescription =
1262 this.webRTCWrapper.setLocalDescription().get();
1263 prepareSessionInitiate(webRTCSessionDescription, targetState);
1264 } catch (final Exception e) {
1265 // TODO sending the error text is worthwhile as well. Especially for FailureToSet
1266 // exceptions
1267 failureToInitiateSession(e, targetState);
1268 }
1269 }
1270
1271 private void failureToInitiateSession(final Throwable throwable, final State targetState) {
1272 if (isTerminated()) {
1273 return;
1274 }
1275 Log.d(
1276 Config.LOGTAG,
1277 id.account.getJid().asBareJid() + ": unable to sendSessionInitiate",
1278 Throwables.getRootCause(throwable));
1279 webRTCWrapper.close();
1280 final Reason reason = Reason.ofThrowable(throwable);
1281 if (isInState(targetState)) {
1282 sendSessionTerminate(reason, throwable.getMessage());
1283 } else {
1284 sendRetract(reason);
1285 }
1286 }
1287
1288 private void sendRetract(final Reason reason) {
1289 // TODO embed reason into retract
1290 sendJingleMessage("retract", id.with.asBareJid());
1291 transitionOrThrow(reasonToState(reason));
1292 this.finish();
1293 }
1294
1295 private void prepareSessionInitiate(
1296 final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) {
1297 final SessionDescription sessionDescription =
1298 SessionDescription.parse(webRTCSessionDescription.description);
1299 final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true);
1300 this.initiatorRtpContentMap = rtpContentMap;
1301 //TODO delay ready to receive ice until after session-init
1302 this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
1303 final ListenableFuture<RtpContentMap> outgoingContentMapFuture =
1304 encryptSessionInitiate(rtpContentMap);
1305 Futures.addCallback(
1306 outgoingContentMapFuture,
1307 new FutureCallback<RtpContentMap>() {
1308 @Override
1309 public void onSuccess(final RtpContentMap outgoingContentMap) {
1310 sendSessionInitiate(outgoingContentMap, targetState);
1311 }
1312
1313 @Override
1314 public void onFailure(@NonNull final Throwable throwable) {
1315 failureToInitiateSession(throwable, targetState);
1316 }
1317 },
1318 MoreExecutors.directExecutor());
1319 }
1320
1321 private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) {
1322 if (isTerminated()) {
1323 Log.w(
1324 Config.LOGTAG,
1325 id.account.getJid().asBareJid()
1326 + ": preparing session was too slow. already terminated. nothing to do.");
1327 return;
1328 }
1329 this.transitionOrThrow(targetState);
1330 final JinglePacket sessionInitiate =
1331 rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
1332 send(sessionInitiate);
1333 }
1334
1335 private ListenableFuture<RtpContentMap> encryptSessionInitiate(
1336 final RtpContentMap rtpContentMap) {
1337 if (this.omemoVerification.hasDeviceId()) {
1338 final ListenableFuture<AxolotlService.OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>>
1339 verifiedPayloadFuture =
1340 id.account
1341 .getAxolotlService()
1342 .encrypt(
1343 rtpContentMap,
1344 id.with,
1345 omemoVerification.getDeviceId());
1346 final ListenableFuture<RtpContentMap> future =
1347 Futures.transform(
1348 verifiedPayloadFuture,
1349 verifiedPayload -> {
1350 omemoVerification.setSessionFingerprint(
1351 verifiedPayload.getFingerprint());
1352 return verifiedPayload.getPayload();
1353 },
1354 MoreExecutors.directExecutor());
1355 if (Config.REQUIRE_RTP_VERIFICATION) {
1356 return future;
1357 }
1358 return Futures.catching(
1359 future,
1360 CryptoFailedException.class,
1361 e -> {
1362 Log.w(
1363 Config.LOGTAG,
1364 id.account.getJid().asBareJid()
1365 + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back",
1366 e);
1367 return rtpContentMap;
1368 },
1369 MoreExecutors.directExecutor());
1370 } else {
1371 return Futures.immediateFuture(rtpContentMap);
1372 }
1373 }
1374
1375 private void sendSessionTerminate(final Reason reason) {
1376 sendSessionTerminate(reason, null);
1377 }
1378
1379 private void sendSessionTerminate(final Reason reason, final String text) {
1380 final State previous = this.state;
1381 final State target = reasonToState(reason);
1382 transitionOrThrow(target);
1383 if (previous != State.NULL) {
1384 writeLogMessage(target);
1385 }
1386 final JinglePacket jinglePacket =
1387 new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
1388 jinglePacket.setReason(reason, text);
1389 Log.d(Config.LOGTAG, jinglePacket.toString());
1390 send(jinglePacket);
1391 finish();
1392 }
1393
1394 private void sendTransportInfo(
1395 final String contentName, IceUdpTransportInfo.Candidate candidate) {
1396 final RtpContentMap transportInfo;
1397 try {
1398 final RtpContentMap rtpContentMap =
1399 isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
1400 transportInfo = rtpContentMap.transportInfo(contentName, candidate);
1401 } catch (final Exception e) {
1402 Log.d(
1403 Config.LOGTAG,
1404 id.account.getJid().asBareJid()
1405 + ": unable to prepare transport-info from candidate for content="
1406 + contentName);
1407 return;
1408 }
1409 final JinglePacket jinglePacket =
1410 transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
1411 send(jinglePacket);
1412 }
1413
1414 private void send(final JinglePacket jinglePacket) {
1415 jinglePacket.setTo(id.with);
1416 xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
1417 }
1418
1419 private synchronized void handleIqResponse(final Account account, final IqPacket response) {
1420 if (response.getType() == IqPacket.TYPE.ERROR) {
1421 handleIqErrorResponse(response);
1422 return;
1423 }
1424 if (response.getType() == IqPacket.TYPE.TIMEOUT) {
1425 handleIqTimeoutResponse(response);
1426 }
1427 }
1428
1429 private void handleIqErrorResponse(final IqPacket response) {
1430 Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
1431 final String errorCondition = response.getErrorCondition();
1432 Log.d(
1433 Config.LOGTAG,
1434 id.account.getJid().asBareJid()
1435 + ": received IQ-error from "
1436 + response.getFrom()
1437 + " in RTP session. "
1438 + errorCondition);
1439 if (isTerminated()) {
1440 Log.i(
1441 Config.LOGTAG,
1442 id.account.getJid().asBareJid()
1443 + ": ignoring error because session was already terminated");
1444 return;
1445 }
1446 this.webRTCWrapper.close();
1447 final State target;
1448 if (Arrays.asList(
1449 "service-unavailable",
1450 "recipient-unavailable",
1451 "remote-server-not-found",
1452 "remote-server-timeout")
1453 .contains(errorCondition)) {
1454 target = State.TERMINATED_CONNECTIVITY_ERROR;
1455 } else {
1456 target = State.TERMINATED_APPLICATION_FAILURE;
1457 }
1458 transitionOrThrow(target);
1459 this.finish();
1460 }
1461
1462 private void handleIqTimeoutResponse(final IqPacket response) {
1463 Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
1464 Log.d(
1465 Config.LOGTAG,
1466 id.account.getJid().asBareJid()
1467 + ": received IQ timeout in RTP session with "
1468 + id.with
1469 + ". terminating with connectivity error");
1470 if (isTerminated()) {
1471 Log.i(
1472 Config.LOGTAG,
1473 id.account.getJid().asBareJid()
1474 + ": ignoring error because session was already terminated");
1475 return;
1476 }
1477 this.webRTCWrapper.close();
1478 transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
1479 this.finish();
1480 }
1481
1482 private void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
1483 Log.d(
1484 Config.LOGTAG,
1485 id.account.getJid().asBareJid() + ": terminating session with out-of-order");
1486 this.webRTCWrapper.close();
1487 transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
1488 respondWithOutOfOrder(jinglePacket);
1489 this.finish();
1490 }
1491
1492 private void respondWithTieBreak(final JinglePacket jinglePacket) {
1493 respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
1494 }
1495
1496 private void respondWithOutOfOrder(final JinglePacket jinglePacket) {
1497 respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
1498 }
1499
1500 void respondWithJingleError(
1501 final IqPacket original,
1502 String jingleCondition,
1503 String condition,
1504 String conditionType) {
1505 jingleConnectionManager.respondWithJingleError(
1506 id.account, original, jingleCondition, condition, conditionType);
1507 }
1508
1509 private void respondOk(final JinglePacket jinglePacket) {
1510 xmppConnectionService.sendIqPacket(
1511 id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
1512 }
1513
1514 public RtpEndUserState getEndUserState() {
1515 switch (this.state) {
1516 case NULL:
1517 case PROPOSED:
1518 case SESSION_INITIALIZED:
1519 if (isInitiator()) {
1520 return RtpEndUserState.RINGING;
1521 } else {
1522 return RtpEndUserState.INCOMING_CALL;
1523 }
1524 case PROCEED:
1525 if (isInitiator()) {
1526 return RtpEndUserState.RINGING;
1527 } else {
1528 return RtpEndUserState.ACCEPTING_CALL;
1529 }
1530 case SESSION_INITIALIZED_PRE_APPROVED:
1531 if (isInitiator()) {
1532 return RtpEndUserState.RINGING;
1533 } else {
1534 return RtpEndUserState.CONNECTING;
1535 }
1536 case SESSION_ACCEPTED:
1537 return getPeerConnectionStateAsEndUserState();
1538 case REJECTED:
1539 case REJECTED_RACED:
1540 case TERMINATED_DECLINED_OR_BUSY:
1541 if (isInitiator()) {
1542 return RtpEndUserState.DECLINED_OR_BUSY;
1543 } else {
1544 return RtpEndUserState.ENDED;
1545 }
1546 case TERMINATED_SUCCESS:
1547 case ACCEPTED:
1548 case RETRACTED:
1549 case TERMINATED_CANCEL_OR_TIMEOUT:
1550 return RtpEndUserState.ENDED;
1551 case RETRACTED_RACED:
1552 if (isInitiator()) {
1553 return RtpEndUserState.ENDED;
1554 } else {
1555 return RtpEndUserState.RETRACTED;
1556 }
1557 case TERMINATED_CONNECTIVITY_ERROR:
1558 return zeroDuration()
1559 ? RtpEndUserState.CONNECTIVITY_ERROR
1560 : RtpEndUserState.CONNECTIVITY_LOST_ERROR;
1561 case TERMINATED_APPLICATION_FAILURE:
1562 return RtpEndUserState.APPLICATION_ERROR;
1563 case TERMINATED_SECURITY_ERROR:
1564 return RtpEndUserState.SECURITY_ERROR;
1565 }
1566 throw new IllegalStateException(
1567 String.format("%s has no equivalent EndUserState", this.state));
1568 }
1569
1570 private RtpEndUserState getPeerConnectionStateAsEndUserState() {
1571 final PeerConnection.PeerConnectionState state;
1572 try {
1573 state = webRTCWrapper.getState();
1574 } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
1575 // We usually close the WebRTCWrapper *before* transitioning so we might still
1576 // be in SESSION_ACCEPTED even though the peerConnection has been torn down
1577 return RtpEndUserState.ENDING_CALL;
1578 }
1579 switch (state) {
1580 case CONNECTED:
1581 return RtpEndUserState.CONNECTED;
1582 case NEW:
1583 case CONNECTING:
1584 return RtpEndUserState.CONNECTING;
1585 case CLOSED:
1586 return RtpEndUserState.ENDING_CALL;
1587 default:
1588 return zeroDuration()
1589 ? RtpEndUserState.CONNECTIVITY_ERROR
1590 : RtpEndUserState.RECONNECTING;
1591 }
1592 }
1593
1594 public Set<Media> getMedia() {
1595 final State current = getState();
1596 if (current == State.NULL) {
1597 if (isInitiator()) {
1598 return Preconditions.checkNotNull(
1599 this.proposedMedia, "RTP connection has not been initialized properly");
1600 }
1601 throw new IllegalStateException("RTP connection has not been initialized yet");
1602 }
1603 if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) {
1604 return Preconditions.checkNotNull(
1605 this.proposedMedia, "RTP connection has not been initialized properly");
1606 }
1607 final RtpContentMap initiatorContentMap = initiatorRtpContentMap;
1608 if (initiatorContentMap != null) {
1609 return initiatorContentMap.getMedia();
1610 } else if (isTerminated()) {
1611 return Collections.emptySet(); // we might fail before we ever got a chance to set media
1612 } else {
1613 return Preconditions.checkNotNull(
1614 this.proposedMedia, "RTP connection has not been initialized properly");
1615 }
1616 }
1617
1618 public boolean isVerified() {
1619 final String fingerprint = this.omemoVerification.getFingerprint();
1620 if (fingerprint == null) {
1621 return false;
1622 }
1623 final FingerprintStatus status =
1624 id.account.getAxolotlService().getFingerprintTrust(fingerprint);
1625 return status != null && status.isVerified();
1626 }
1627
1628 public synchronized void acceptCall() {
1629 switch (this.state) {
1630 case PROPOSED:
1631 cancelRingingTimeout();
1632 acceptCallFromProposed();
1633 break;
1634 case SESSION_INITIALIZED:
1635 cancelRingingTimeout();
1636 acceptCallFromSessionInitialized();
1637 break;
1638 case ACCEPTED:
1639 Log.w(
1640 Config.LOGTAG,
1641 id.account.getJid().asBareJid()
1642 + ": the call has already been accepted with another client. UI was just lagging behind");
1643 break;
1644 case PROCEED:
1645 case SESSION_ACCEPTED:
1646 Log.w(
1647 Config.LOGTAG,
1648 id.account.getJid().asBareJid()
1649 + ": the call has already been accepted. user probably double tapped the UI");
1650 break;
1651 default:
1652 throw new IllegalStateException("Can not accept call from " + this.state);
1653 }
1654 }
1655
1656 public void notifyPhoneCall() {
1657 Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections");
1658 if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) {
1659 rejectCall();
1660 } else {
1661 endCall();
1662 }
1663 }
1664
1665 public synchronized void rejectCall() {
1666 if (isTerminated()) {
1667 Log.w(
1668 Config.LOGTAG,
1669 id.account.getJid().asBareJid()
1670 + ": received rejectCall() when session has already been terminated. nothing to do");
1671 return;
1672 }
1673 switch (this.state) {
1674 case PROPOSED:
1675 rejectCallFromProposed();
1676 break;
1677 case SESSION_INITIALIZED:
1678 rejectCallFromSessionInitiate();
1679 break;
1680 default:
1681 throw new IllegalStateException("Can not reject call from " + this.state);
1682 }
1683 }
1684
1685 public synchronized void endCall() {
1686 if (isTerminated()) {
1687 Log.w(
1688 Config.LOGTAG,
1689 id.account.getJid().asBareJid()
1690 + ": received endCall() when session has already been terminated. nothing to do");
1691 return;
1692 }
1693 if (isInState(State.PROPOSED) && !isInitiator()) {
1694 rejectCallFromProposed();
1695 return;
1696 }
1697 if (isInState(State.PROCEED)) {
1698 if (isInitiator()) {
1699 retractFromProceed();
1700 } else {
1701 rejectCallFromProceed();
1702 }
1703 return;
1704 }
1705 if (isInitiator()
1706 && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) {
1707 this.webRTCWrapper.close();
1708 sendSessionTerminate(Reason.CANCEL);
1709 return;
1710 }
1711 if (isInState(State.SESSION_INITIALIZED)) {
1712 rejectCallFromSessionInitiate();
1713 return;
1714 }
1715 if (isInState(State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
1716 this.webRTCWrapper.close();
1717 sendSessionTerminate(Reason.SUCCESS);
1718 return;
1719 }
1720 if (isInState(
1721 State.TERMINATED_APPLICATION_FAILURE,
1722 State.TERMINATED_CONNECTIVITY_ERROR,
1723 State.TERMINATED_DECLINED_OR_BUSY)) {
1724 Log.d(
1725 Config.LOGTAG,
1726 "ignoring request to end call because already in state " + this.state);
1727 return;
1728 }
1729 throw new IllegalStateException(
1730 "called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator());
1731 }
1732
1733 private void retractFromProceed() {
1734 Log.d(Config.LOGTAG, "retract from proceed");
1735 this.sendJingleMessage("retract");
1736 closeTransitionLogFinish(State.RETRACTED_RACED);
1737 }
1738
1739 private void closeTransitionLogFinish(final State state) {
1740 this.webRTCWrapper.close();
1741 transitionOrThrow(state);
1742 writeLogMessage(state);
1743 finish();
1744 }
1745
1746 private void setupWebRTC(
1747 final Set<Media> media, final List<PeerConnection.IceServer> iceServers)
1748 throws WebRTCWrapper.InitializationException {
1749 this.jingleConnectionManager.ensureConnectionIsRegistered(this);
1750 final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference;
1751 if (media.contains(Media.VIDEO)) {
1752 speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.SPEAKER;
1753 } else {
1754 speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.EARPIECE;
1755 }
1756 this.webRTCWrapper.setup(this.xmppConnectionService, speakerPhonePreference);
1757 this.webRTCWrapper.initializePeerConnection(media, iceServers);
1758 }
1759
1760 private void acceptCallFromProposed() {
1761 transitionOrThrow(State.PROCEED);
1762 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1763 this.sendJingleMessage("accept", id.account.getJid().asBareJid());
1764 this.sendJingleMessage("proceed");
1765 }
1766
1767 private void rejectCallFromProposed() {
1768 transitionOrThrow(State.REJECTED);
1769 writeLogMessageMissed();
1770 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1771 this.sendJingleMessage("reject");
1772 finish();
1773 }
1774
1775 private void rejectCallFromProceed() {
1776 this.sendJingleMessage("reject");
1777 closeTransitionLogFinish(State.REJECTED_RACED);
1778 }
1779
1780 private void rejectCallFromSessionInitiate() {
1781 webRTCWrapper.close();
1782 sendSessionTerminate(Reason.DECLINE);
1783 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1784 }
1785
1786 private void sendJingleMessage(final String action) {
1787 sendJingleMessage(action, id.with);
1788 }
1789
1790 private void sendJingleMessage(final String action, final Jid to) {
1791 final MessagePacket messagePacket = new MessagePacket();
1792 messagePacket.setType(MessagePacket.TYPE_CHAT); // we want to carbon copy those
1793 messagePacket.setTo(to);
1794 final Element intent =
1795 messagePacket
1796 .addChild(action, Namespace.JINGLE_MESSAGE)
1797 .setAttribute("id", id.sessionId);
1798 if ("proceed".equals(action)) {
1799 messagePacket.setId(JINGLE_MESSAGE_PROCEED_ID_PREFIX + id.sessionId);
1800 if (isOmemoEnabled()) {
1801 final int deviceId = id.account.getAxolotlService().getOwnDeviceId();
1802 final Element device =
1803 intent.addChild("device", Namespace.OMEMO_DTLS_SRTP_VERIFICATION);
1804 device.setAttribute("id", deviceId);
1805 }
1806 }
1807 messagePacket.addChild("store", "urn:xmpp:hints");
1808 xmppConnectionService.sendMessagePacket(id.account, messagePacket);
1809 }
1810
1811 private boolean isOmemoEnabled() {
1812 final Conversational conversational = message.getConversation();
1813 if (conversational instanceof Conversation) {
1814 return ((Conversation) conversational).getNextEncryption()
1815 == Message.ENCRYPTION_AXOLOTL;
1816 }
1817 return false;
1818 }
1819
1820 private void acceptCallFromSessionInitialized() {
1821 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1822 sendSessionAccept();
1823 }
1824
1825 private synchronized boolean isInState(State... state) {
1826 return Arrays.asList(state).contains(this.state);
1827 }
1828
1829 private boolean transition(final State target) {
1830 return transition(target, null);
1831 }
1832
1833 private synchronized boolean transition(final State target, final Runnable runnable) {
1834 final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
1835 if (validTransitions != null && validTransitions.contains(target)) {
1836 this.state = target;
1837 if (runnable != null) {
1838 runnable.run();
1839 }
1840 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
1841 updateEndUserState();
1842 updateOngoingCallNotification();
1843 return true;
1844 } else {
1845 return false;
1846 }
1847 }
1848
1849 void transitionOrThrow(final State target) {
1850 if (!transition(target)) {
1851 throw new IllegalStateException(
1852 String.format("Unable to transition from %s to %s", this.state, target));
1853 }
1854 }
1855
1856 @Override
1857 public void onIceCandidate(final IceCandidate iceCandidate) {
1858 final RtpContentMap rtpContentMap =
1859 isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
1860 final IceUdpTransportInfo.Credentials credentials;
1861 try {
1862 credentials = rtpContentMap.getCredentials(iceCandidate.sdpMid);
1863 } catch (final IllegalArgumentException e) {
1864 Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate, e);
1865 return;
1866 }
1867 final String uFrag = credentials.ufrag;
1868 final IceUdpTransportInfo.Candidate candidate =
1869 IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, uFrag);
1870 if (candidate == null) {
1871 Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate);
1872 return;
1873 }
1874 Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate);
1875 sendTransportInfo(iceCandidate.sdpMid, candidate);
1876 }
1877
1878 @Override
1879 public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
1880 Log.d(
1881 Config.LOGTAG,
1882 id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState);
1883 this.stateHistory.add(newState);
1884 if (newState == PeerConnection.PeerConnectionState.CONNECTED) {
1885 this.sessionDuration.start();
1886 updateOngoingCallNotification();
1887 } else if (this.sessionDuration.isRunning()) {
1888 this.sessionDuration.stop();
1889 updateOngoingCallNotification();
1890 }
1891
1892 final boolean neverConnected =
1893 !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED);
1894
1895 if (newState == PeerConnection.PeerConnectionState.FAILED) {
1896 if (neverConnected) {
1897 if (isTerminated()) {
1898 Log.d(
1899 Config.LOGTAG,
1900 id.account.getJid().asBareJid()
1901 + ": not sending session-terminate after connectivity error because session is already in state "
1902 + this.state);
1903 return;
1904 }
1905 webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection);
1906 return;
1907 } else {
1908 webRTCWrapper.restartIce();
1909 }
1910 }
1911 updateEndUserState();
1912 }
1913
1914 @Override
1915 public void onRenegotiationNeeded() {
1916 this.webRTCWrapper.execute(this::renegotiate);
1917 }
1918
1919 private void renegotiate() {
1920 //TODO needs to be called only for ice restarts; maybe in the call to restartICe()
1921 this.stateHistory.clear();
1922 this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
1923 final SessionDescription sessionDescription;
1924 try {
1925 sessionDescription = setLocalSessionDescription();
1926 } catch (final Exception e) {
1927 final Throwable cause = Throwables.getRootCause(e);
1928 Log.d(Config.LOGTAG, "failed to renegotiate", cause);
1929 webRTCWrapper.close();
1930 sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
1931 return;
1932 }
1933 final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, isInitiator());
1934 final RtpContentMap currentContentMap = getLocalContentMap();
1935 final boolean iceRestart = currentContentMap.iceRestart(rtpContentMap);
1936 final RtpContentMap.Diff diff = currentContentMap.diff(rtpContentMap);
1937
1938 Log.d(
1939 Config.LOGTAG,
1940 id.getAccount().getJid().asBareJid()
1941 + ": renegotiate. iceRestart="
1942 + iceRestart
1943 + " content id diff="
1944 + diff);
1945
1946 if (diff.hasModifications() && iceRestart) {
1947 webRTCWrapper.close();
1948 sendSessionTerminate(Reason.FAILED_APPLICATION, "WebRTC unexpectedly tried to modify content and transport at once");
1949 return;
1950 }
1951
1952 if (iceRestart) {
1953 initiateIceRestart(rtpContentMap);
1954 return;
1955 }
1956
1957 if (diff.added.size() > 0) {
1958 sendContentAdd(rtpContentMap);
1959 }
1960
1961 }
1962
1963 private void initiateIceRestart(final RtpContentMap rtpContentMap) {
1964 final RtpContentMap transportInfo = rtpContentMap.transportInfo();
1965 final JinglePacket jinglePacket =
1966 transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
1967 Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket);
1968 jinglePacket.setTo(id.with);
1969 xmppConnectionService.sendIqPacket(
1970 id.account,
1971 jinglePacket,
1972 (account, response) -> {
1973 if (response.getType() == IqPacket.TYPE.RESULT) {
1974 Log.d(Config.LOGTAG, "received success to our ice restart");
1975 setLocalContentMap(rtpContentMap);
1976 webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
1977 return;
1978 }
1979 if (response.getType() == IqPacket.TYPE.ERROR) {
1980 final Element error = response.findChild("error");
1981 if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) {
1982 Log.d(Config.LOGTAG, "received tie-break as result of ice restart");
1983 return;
1984 }
1985 handleIqErrorResponse(response);
1986 }
1987 if (response.getType() == IqPacket.TYPE.TIMEOUT) {
1988 handleIqTimeoutResponse(response);
1989 }
1990 });
1991 }
1992
1993 private void sendContentAdd(final RtpContentMap rtpContentMap) {
1994
1995 }
1996
1997 private void setLocalContentMap(final RtpContentMap rtpContentMap) {
1998 if (isInitiator()) {
1999 this.initiatorRtpContentMap = rtpContentMap;
2000 } else {
2001 this.responderRtpContentMap = rtpContentMap;
2002 }
2003 }
2004
2005 private void setRemoteContentMap(final RtpContentMap rtpContentMap) {
2006 if (isInitiator()) {
2007 this.responderRtpContentMap = rtpContentMap;
2008 } else {
2009 this.initiatorRtpContentMap = rtpContentMap;
2010 }
2011 }
2012
2013 private SessionDescription setLocalSessionDescription()
2014 throws ExecutionException, InterruptedException {
2015 final org.webrtc.SessionDescription sessionDescription =
2016 this.webRTCWrapper.setLocalDescription().get();
2017 return SessionDescription.parse(sessionDescription.description);
2018 }
2019
2020 private void closeWebRTCSessionAfterFailedConnection() {
2021 this.webRTCWrapper.close();
2022 synchronized (this) {
2023 if (isTerminated()) {
2024 Log.d(
2025 Config.LOGTAG,
2026 id.account.getJid().asBareJid()
2027 + ": no need to send session-terminate after failed connection. Other party already did");
2028 return;
2029 }
2030 sendSessionTerminate(Reason.CONNECTIVITY_ERROR);
2031 }
2032 }
2033
2034 public boolean zeroDuration() {
2035 return this.sessionDuration.elapsed(TimeUnit.NANOSECONDS) <= 0;
2036 }
2037
2038 public long getCallDuration() {
2039 return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS);
2040 }
2041
2042 public AppRTCAudioManager getAudioManager() {
2043 return webRTCWrapper.getAudioManager();
2044 }
2045
2046 public boolean isMicrophoneEnabled() {
2047 return webRTCWrapper.isMicrophoneEnabled();
2048 }
2049
2050 public boolean setMicrophoneEnabled(final boolean enabled) {
2051 return webRTCWrapper.setMicrophoneEnabled(enabled);
2052 }
2053
2054 public boolean isVideoEnabled() {
2055 return webRTCWrapper.isVideoEnabled();
2056 }
2057
2058 public void setVideoEnabled(final boolean enabled) {
2059 webRTCWrapper.setVideoEnabled(enabled);
2060 }
2061
2062 public boolean isCameraSwitchable() {
2063 return webRTCWrapper.isCameraSwitchable();
2064 }
2065
2066 public boolean isFrontCamera() {
2067 return webRTCWrapper.isFrontCamera();
2068 }
2069
2070 public ListenableFuture<Boolean> switchCamera() {
2071 return webRTCWrapper.switchCamera();
2072 }
2073
2074 @Override
2075 public void onAudioDeviceChanged(
2076 AppRTCAudioManager.AudioDevice selectedAudioDevice,
2077 Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
2078 xmppConnectionService.notifyJingleRtpConnectionUpdate(
2079 selectedAudioDevice, availableAudioDevices);
2080 }
2081
2082 private void updateEndUserState() {
2083 final RtpEndUserState endUserState = getEndUserState();
2084 jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia());
2085 xmppConnectionService.notifyJingleRtpConnectionUpdate(
2086 id.account, id.with, id.sessionId, endUserState);
2087 }
2088
2089 private void updateOngoingCallNotification() {
2090 final State state = this.state;
2091 if (STATES_SHOWING_ONGOING_CALL.contains(state)) {
2092 final boolean reconnecting;
2093 if (state == State.SESSION_ACCEPTED) {
2094 reconnecting =
2095 getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING;
2096 } else {
2097 reconnecting = false;
2098 }
2099 xmppConnectionService.setOngoingCall(id, getMedia(), reconnecting);
2100 } else {
2101 xmppConnectionService.removeOngoingCall();
2102 }
2103 }
2104
2105 private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) {
2106 if (id.account.getXmppConnection().getFeatures().externalServiceDiscovery()) {
2107 final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2108 request.setTo(id.account.getDomain());
2109 request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
2110 xmppConnectionService.sendIqPacket(
2111 id.account,
2112 request,
2113 (account, response) -> {
2114 ImmutableList.Builder<PeerConnection.IceServer> listBuilder =
2115 new ImmutableList.Builder<>();
2116 if (response.getType() == IqPacket.TYPE.RESULT) {
2117 final Element services =
2118 response.findChild(
2119 "services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
2120 final List<Element> children =
2121 services == null
2122 ? Collections.emptyList()
2123 : services.getChildren();
2124 for (final Element child : children) {
2125 if ("service".equals(child.getName())) {
2126 final String type = child.getAttribute("type");
2127 final String host = child.getAttribute("host");
2128 final String sport = child.getAttribute("port");
2129 final Integer port =
2130 sport == null ? null : Ints.tryParse(sport);
2131 final String transport = child.getAttribute("transport");
2132 final String username = child.getAttribute("username");
2133 final String password = child.getAttribute("password");
2134 if (Strings.isNullOrEmpty(host) || port == null) {
2135 continue;
2136 }
2137 if (port < 0 || port > 65535) {
2138 continue;
2139 }
2140 if (Arrays.asList("stun", "stuns", "turn", "turns")
2141 .contains(type)
2142 && Arrays.asList("udp", "tcp").contains(transport)) {
2143 if (Arrays.asList("stuns", "turns").contains(type)
2144 && "udp".equals(transport)) {
2145 Log.d(
2146 Config.LOGTAG,
2147 id.account.getJid().asBareJid()
2148 + ": skipping invalid combination of udp/tls in external services");
2149 continue;
2150 }
2151 final PeerConnection.IceServer.Builder iceServerBuilder =
2152 PeerConnection.IceServer.builder(
2153 String.format(
2154 "%s:%s:%s?transport=%s",
2155 type,
2156 IP.wrapIPv6(host),
2157 port,
2158 transport));
2159 iceServerBuilder.setTlsCertPolicy(
2160 PeerConnection.TlsCertPolicy
2161 .TLS_CERT_POLICY_INSECURE_NO_CHECK);
2162 if (username != null && password != null) {
2163 iceServerBuilder.setUsername(username);
2164 iceServerBuilder.setPassword(password);
2165 } else if (Arrays.asList("turn", "turns").contains(type)) {
2166 // The WebRTC spec requires throwing an
2167 // InvalidAccessError when username (from libwebrtc
2168 // source coder)
2169 // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
2170 Log.d(
2171 Config.LOGTAG,
2172 id.account.getJid().asBareJid()
2173 + ": skipping "
2174 + type
2175 + "/"
2176 + transport
2177 + " without username and password");
2178 continue;
2179 }
2180 final PeerConnection.IceServer iceServer =
2181 iceServerBuilder.createIceServer();
2182 Log.d(
2183 Config.LOGTAG,
2184 id.account.getJid().asBareJid()
2185 + ": discovered ICE Server: "
2186 + iceServer);
2187 listBuilder.add(iceServer);
2188 }
2189 }
2190 }
2191 }
2192 final List<PeerConnection.IceServer> iceServers = listBuilder.build();
2193 if (iceServers.size() == 0) {
2194 Log.w(
2195 Config.LOGTAG,
2196 id.account.getJid().asBareJid()
2197 + ": no ICE server found "
2198 + response);
2199 }
2200 onIceServersDiscovered.onIceServersDiscovered(iceServers);
2201 });
2202 } else {
2203 Log.w(
2204 Config.LOGTAG,
2205 id.account.getJid().asBareJid() + ": has no external service discovery");
2206 onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList());
2207 }
2208 }
2209
2210 private void finish() {
2211 if (isTerminated()) {
2212 this.cancelRingingTimeout();
2213 this.webRTCWrapper.verifyClosed();
2214 this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
2215 this.jingleConnectionManager.finishConnectionOrThrow(this);
2216 } else {
2217 throw new IllegalStateException(
2218 String.format("Unable to call finish from %s", this.state));
2219 }
2220 }
2221
2222 private void writeLogMessage(final State state) {
2223 final long duration = getCallDuration();
2224 if (state == State.TERMINATED_SUCCESS
2225 || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) {
2226 writeLogMessageSuccess(duration);
2227 } else {
2228 writeLogMessageMissed();
2229 }
2230 }
2231
2232 private void writeLogMessageSuccess(final long duration) {
2233 this.message.setBody(new RtpSessionStatus(true, duration).toString());
2234 this.writeMessage();
2235 }
2236
2237 private void writeLogMessageMissed() {
2238 this.message.setBody(new RtpSessionStatus(false, 0).toString());
2239 this.writeMessage();
2240 }
2241
2242 private void writeMessage() {
2243 final Conversational conversational = message.getConversation();
2244 if (conversational instanceof Conversation) {
2245 ((Conversation) conversational).add(this.message);
2246 xmppConnectionService.createMessageAsync(message);
2247 xmppConnectionService.updateConversationUi();
2248 } else {
2249 throw new IllegalStateException("Somehow the conversation in a message was a stub");
2250 }
2251 }
2252
2253 public State getState() {
2254 return this.state;
2255 }
2256
2257 boolean isTerminated() {
2258 return TERMINATED.contains(this.state);
2259 }
2260
2261 public Optional<VideoTrack> getLocalVideoTrack() {
2262 return webRTCWrapper.getLocalVideoTrack();
2263 }
2264
2265 public Optional<VideoTrack> getRemoteVideoTrack() {
2266 return webRTCWrapper.getRemoteVideoTrack();
2267 }
2268
2269 public EglBase.Context getEglBaseContext() {
2270 return webRTCWrapper.getEglBaseContext();
2271 }
2272
2273 void setProposedMedia(final Set<Media> media) {
2274 this.proposedMedia = media;
2275 }
2276
2277 public void fireStateUpdate() {
2278 final RtpEndUserState endUserState = getEndUserState();
2279 xmppConnectionService.notifyJingleRtpConnectionUpdate(
2280 id.account, id.with, id.sessionId, endUserState);
2281 }
2282
2283 private interface OnIceServersDiscovered {
2284 void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
2285 }
2286}