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.Stopwatch;
11import com.google.common.base.Strings;
12import com.google.common.base.Throwables;
13import com.google.common.collect.Collections2;
14import com.google.common.collect.ImmutableList;
15import com.google.common.collect.ImmutableMap;
16import com.google.common.collect.Sets;
17import com.google.common.primitives.Ints;
18import com.google.common.util.concurrent.FutureCallback;
19import com.google.common.util.concurrent.Futures;
20import com.google.common.util.concurrent.ListenableFuture;
21import com.google.common.util.concurrent.MoreExecutors;
22
23import org.webrtc.EglBase;
24import org.webrtc.IceCandidate;
25import org.webrtc.PeerConnection;
26import org.webrtc.VideoTrack;
27
28import java.util.Arrays;
29import java.util.Collection;
30import java.util.Collections;
31import java.util.LinkedList;
32import java.util.List;
33import java.util.Map;
34import java.util.Queue;
35import java.util.Set;
36import java.util.Timer;
37import java.util.TimerTask;
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 IceUdpTransportInfo.Credentials existingCredentials;
353 final IceUdpTransportInfo.Credentials newCredentials;
354 try {
355 existingCredentials = existing.getCredentials();
356 newCredentials = rtpContentMap.getCredentials();
357 } catch (final IllegalStateException e) {
358 Log.d(Config.LOGTAG, "unable to gather credentials for comparison", e);
359 return false;
360 }
361 if (existingCredentials.equals(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);
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));
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 List<String> toIdentificationTags(final RtpContentMap rtpContentMap) {
505 final Group originalGroup = rtpContentMap.group;
506 final List<String> identificationTags =
507 originalGroup == null
508 ? rtpContentMap.getNames()
509 : originalGroup.getIdentificationTags();
510 if (identificationTags.size() == 0) {
511 Log.w(
512 Config.LOGTAG,
513 id.account.getJid().asBareJid()
514 + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices");
515 }
516 return identificationTags;
517 }
518
519 private ListenableFuture<RtpContentMap> receiveRtpContentMap(
520 final JinglePacket jinglePacket, final boolean expectVerification) {
521 final RtpContentMap receivedContentMap;
522 try {
523 receivedContentMap = RtpContentMap.of(jinglePacket);
524 } catch (final Exception e) {
525 return Futures.immediateFailedFuture(e);
526 }
527 if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) {
528 final ListenableFuture<AxolotlService.OmemoVerifiedPayload<RtpContentMap>> future =
529 id.account
530 .getAxolotlService()
531 .decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with);
532 return Futures.transform(
533 future,
534 omemoVerifiedPayload -> {
535 // TODO test if an exception here triggers a correct abort
536 omemoVerification.setOrEnsureEqual(omemoVerifiedPayload);
537 Log.d(
538 Config.LOGTAG,
539 id.account.getJid().asBareJid()
540 + ": received verifiable DTLS fingerprint via "
541 + omemoVerification);
542 return omemoVerifiedPayload.getPayload();
543 },
544 MoreExecutors.directExecutor());
545 } else if (Config.REQUIRE_RTP_VERIFICATION || expectVerification) {
546 return Futures.immediateFailedFuture(
547 new SecurityException("DTLS fingerprint was unexpectedly not verifiable"));
548 } else {
549 return Futures.immediateFuture(receivedContentMap);
550 }
551 }
552
553 private void receiveSessionInitiate(final JinglePacket jinglePacket) {
554 if (isInitiator()) {
555 Log.d(
556 Config.LOGTAG,
557 String.format(
558 "%s: received session-initiate even though we were initiating",
559 id.account.getJid().asBareJid()));
560 if (isTerminated()) {
561 Log.d(
562 Config.LOGTAG,
563 String.format(
564 "%s: got a reason to terminate with out-of-order. but already in state %s",
565 id.account.getJid().asBareJid(), getState()));
566 respondWithOutOfOrder(jinglePacket);
567 } else {
568 terminateWithOutOfOrder(jinglePacket);
569 }
570 return;
571 }
572 final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
573 Futures.addCallback(
574 future,
575 new FutureCallback<RtpContentMap>() {
576 @Override
577 public void onSuccess(@Nullable RtpContentMap rtpContentMap) {
578 receiveSessionInitiate(jinglePacket, rtpContentMap);
579 }
580
581 @Override
582 public void onFailure(@NonNull final Throwable throwable) {
583 respondOk(jinglePacket);
584 sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage());
585 }
586 },
587 MoreExecutors.directExecutor());
588 }
589
590 private void receiveSessionInitiate(
591 final JinglePacket jinglePacket, final RtpContentMap contentMap) {
592 try {
593 contentMap.requireContentDescriptions();
594 contentMap.requireDTLSFingerprint(true);
595 } catch (final RuntimeException e) {
596 Log.d(
597 Config.LOGTAG,
598 id.account.getJid().asBareJid() + ": improperly formatted contents",
599 Throwables.getRootCause(e));
600 respondOk(jinglePacket);
601 sendSessionTerminate(Reason.of(e), e.getMessage());
602 return;
603 }
604 Log.d(
605 Config.LOGTAG,
606 "processing session-init with " + contentMap.contents.size() + " contents");
607 final State target;
608 if (this.state == State.PROCEED) {
609 Preconditions.checkState(
610 proposedMedia != null && proposedMedia.size() > 0,
611 "proposed media must be set when processing pre-approved session-initiate");
612 if (!this.proposedMedia.equals(contentMap.getMedia())) {
613 sendSessionTerminate(
614 Reason.SECURITY_ERROR,
615 String.format(
616 "Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s",
617 this.proposedMedia, contentMap.getMedia()));
618 return;
619 }
620 target = State.SESSION_INITIALIZED_PRE_APPROVED;
621 } else {
622 target = State.SESSION_INITIALIZED;
623 }
624 if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
625 respondOk(jinglePacket);
626 pendingIceCandidates.addAll(contentMap.contents.entrySet());
627 if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
628 Log.d(
629 Config.LOGTAG,
630 id.account.getJid().asBareJid()
631 + ": automatically accepting session-initiate");
632 sendSessionAccept();
633 } else {
634 Log.d(
635 Config.LOGTAG,
636 id.account.getJid().asBareJid()
637 + ": received not pre-approved session-initiate. start ringing");
638 startRinging();
639 }
640 } else {
641 Log.d(
642 Config.LOGTAG,
643 String.format(
644 "%s: received session-initiate while in state %s",
645 id.account.getJid().asBareJid(), state));
646 terminateWithOutOfOrder(jinglePacket);
647 }
648 }
649
650 private void receiveSessionAccept(final JinglePacket jinglePacket) {
651 if (!isInitiator()) {
652 Log.d(
653 Config.LOGTAG,
654 String.format(
655 "%s: received session-accept even though we were responding",
656 id.account.getJid().asBareJid()));
657 terminateWithOutOfOrder(jinglePacket);
658 return;
659 }
660 final ListenableFuture<RtpContentMap> future =
661 receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint());
662 Futures.addCallback(
663 future,
664 new FutureCallback<RtpContentMap>() {
665 @Override
666 public void onSuccess(@Nullable RtpContentMap rtpContentMap) {
667 receiveSessionAccept(jinglePacket, rtpContentMap);
668 }
669
670 @Override
671 public void onFailure(@NonNull final Throwable throwable) {
672 respondOk(jinglePacket);
673 Log.d(
674 Config.LOGTAG,
675 id.account.getJid().asBareJid()
676 + ": improperly formatted contents in session-accept",
677 throwable);
678 webRTCWrapper.close();
679 sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage());
680 }
681 },
682 MoreExecutors.directExecutor());
683 }
684
685 private void receiveSessionAccept(
686 final JinglePacket jinglePacket, final RtpContentMap contentMap) {
687 try {
688 contentMap.requireContentDescriptions();
689 contentMap.requireDTLSFingerprint();
690 } catch (final RuntimeException e) {
691 respondOk(jinglePacket);
692 Log.d(
693 Config.LOGTAG,
694 id.account.getJid().asBareJid()
695 + ": improperly formatted contents in session-accept",
696 e);
697 webRTCWrapper.close();
698 sendSessionTerminate(Reason.of(e), e.getMessage());
699 return;
700 }
701 final Set<Media> initiatorMedia = this.initiatorRtpContentMap.getMedia();
702 if (!initiatorMedia.equals(contentMap.getMedia())) {
703 sendSessionTerminate(
704 Reason.SECURITY_ERROR,
705 String.format(
706 "Your session-included included media %s but our session-initiate was %s",
707 this.proposedMedia, contentMap.getMedia()));
708 return;
709 }
710 Log.d(
711 Config.LOGTAG,
712 "processing session-accept with " + contentMap.contents.size() + " contents");
713 if (transition(State.SESSION_ACCEPTED)) {
714 respondOk(jinglePacket);
715 receiveSessionAccept(contentMap);
716 } else {
717 Log.d(
718 Config.LOGTAG,
719 String.format(
720 "%s: received session-accept while in state %s",
721 id.account.getJid().asBareJid(), state));
722 respondOk(jinglePacket);
723 }
724 }
725
726 private void receiveSessionAccept(final RtpContentMap contentMap) {
727 this.responderRtpContentMap = contentMap;
728 this.storePeerDtlsSetup(contentMap.getDtlsSetup());
729 final SessionDescription sessionDescription;
730 try {
731 sessionDescription = SessionDescription.of(contentMap);
732 } catch (final IllegalArgumentException | NullPointerException e) {
733 Log.d(
734 Config.LOGTAG,
735 id.account.getJid().asBareJid()
736 + ": unable convert offer from session-accept to SDP",
737 e);
738 webRTCWrapper.close();
739 sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
740 return;
741 }
742 final org.webrtc.SessionDescription answer =
743 new org.webrtc.SessionDescription(
744 org.webrtc.SessionDescription.Type.ANSWER, sessionDescription.toString());
745 try {
746 this.webRTCWrapper.setRemoteDescription(answer).get();
747 } catch (final Exception e) {
748 Log.d(
749 Config.LOGTAG,
750 id.account.getJid().asBareJid()
751 + ": unable to set remote description after receiving session-accept",
752 Throwables.getRootCause(e));
753 webRTCWrapper.close();
754 sendSessionTerminate(
755 Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage());
756 return;
757 }
758 processCandidates(contentMap.contents.entrySet());
759 }
760
761 private void sendSessionAccept() {
762 final RtpContentMap rtpContentMap = this.initiatorRtpContentMap;
763 if (rtpContentMap == null) {
764 throw new IllegalStateException("initiator RTP Content Map has not been set");
765 }
766 final SessionDescription offer;
767 try {
768 offer = SessionDescription.of(rtpContentMap);
769 } catch (final IllegalArgumentException | NullPointerException e) {
770 Log.d(
771 Config.LOGTAG,
772 id.account.getJid().asBareJid()
773 + ": unable convert offer from session-initiate to SDP",
774 e);
775 webRTCWrapper.close();
776 sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
777 return;
778 }
779 sendSessionAccept(rtpContentMap.getMedia(), offer);
780 }
781
782 private void sendSessionAccept(final Set<Media> media, final SessionDescription offer) {
783 discoverIceServers(iceServers -> sendSessionAccept(media, offer, iceServers));
784 }
785
786 private synchronized void sendSessionAccept(
787 final Set<Media> media,
788 final SessionDescription offer,
789 final List<PeerConnection.IceServer> iceServers) {
790 if (isTerminated()) {
791 Log.w(
792 Config.LOGTAG,
793 id.account.getJid().asBareJid()
794 + ": ICE servers got discovered when session was already terminated. nothing to do.");
795 return;
796 }
797 try {
798 setupWebRTC(media, iceServers);
799 } catch (final WebRTCWrapper.InitializationException e) {
800 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC");
801 webRTCWrapper.close();
802 sendSessionTerminate(Reason.FAILED_APPLICATION);
803 return;
804 }
805 final org.webrtc.SessionDescription sdp =
806 new org.webrtc.SessionDescription(
807 org.webrtc.SessionDescription.Type.OFFER, offer.toString());
808 try {
809 this.webRTCWrapper.setRemoteDescription(sdp).get();
810 addIceCandidatesFromBlackLog();
811 org.webrtc.SessionDescription webRTCSessionDescription =
812 this.webRTCWrapper.setLocalDescription().get();
813 prepareSessionAccept(webRTCSessionDescription);
814 } catch (final Exception e) {
815 failureToAcceptSession(e);
816 }
817 }
818
819 private void failureToAcceptSession(final Throwable throwable) {
820 if (isTerminated()) {
821 return;
822 }
823 final Throwable rootCause = Throwables.getRootCause(throwable);
824 Log.d(Config.LOGTAG, "unable to send session accept", rootCause);
825 webRTCWrapper.close();
826 sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
827 }
828
829 private void addIceCandidatesFromBlackLog() {
830 Map.Entry<String, RtpContentMap.DescriptionTransport> foo;
831 while ((foo = this.pendingIceCandidates.poll()) != null) {
832 processCandidate(foo);
833 Log.d(
834 Config.LOGTAG,
835 id.account.getJid().asBareJid() + ": added candidate from back log");
836 }
837 }
838
839 private void prepareSessionAccept(
840 final org.webrtc.SessionDescription webRTCSessionDescription) {
841 final SessionDescription sessionDescription =
842 SessionDescription.parse(webRTCSessionDescription.description);
843 final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
844 this.responderRtpContentMap = respondingRtpContentMap;
845 storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip());
846 webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
847 final ListenableFuture<RtpContentMap> outgoingContentMapFuture =
848 prepareOutgoingContentMap(respondingRtpContentMap);
849 Futures.addCallback(
850 outgoingContentMapFuture,
851 new FutureCallback<RtpContentMap>() {
852 @Override
853 public void onSuccess(final RtpContentMap outgoingContentMap) {
854 sendSessionAccept(outgoingContentMap);
855 }
856
857 @Override
858 public void onFailure(@NonNull Throwable throwable) {
859 failureToAcceptSession(throwable);
860 }
861 },
862 MoreExecutors.directExecutor());
863 }
864
865 private void sendSessionAccept(final RtpContentMap rtpContentMap) {
866 if (isTerminated()) {
867 Log.w(
868 Config.LOGTAG,
869 id.account.getJid().asBareJid()
870 + ": preparing session accept was too slow. already terminated. nothing to do.");
871 return;
872 }
873 transitionOrThrow(State.SESSION_ACCEPTED);
874 final JinglePacket sessionAccept =
875 rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
876 send(sessionAccept);
877 }
878
879 private ListenableFuture<RtpContentMap> prepareOutgoingContentMap(
880 final RtpContentMap rtpContentMap) {
881 if (this.omemoVerification.hasDeviceId()) {
882 ListenableFuture<AxolotlService.OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>>
883 verifiedPayloadFuture =
884 id.account
885 .getAxolotlService()
886 .encrypt(
887 rtpContentMap,
888 id.with,
889 omemoVerification.getDeviceId());
890 return Futures.transform(
891 verifiedPayloadFuture,
892 verifiedPayload -> {
893 omemoVerification.setOrEnsureEqual(verifiedPayload);
894 return verifiedPayload.getPayload();
895 },
896 MoreExecutors.directExecutor());
897 } else {
898 return Futures.immediateFuture(rtpContentMap);
899 }
900 }
901
902 synchronized void deliveryMessage(
903 final Jid from,
904 final Element message,
905 final String serverMessageId,
906 final long timestamp) {
907 Log.d(
908 Config.LOGTAG,
909 id.account.getJid().asBareJid()
910 + ": delivered message to JingleRtpConnection "
911 + message);
912 switch (message.getName()) {
913 case "propose":
914 receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp);
915 break;
916 case "proceed":
917 receiveProceed(from, Proceed.upgrade(message), serverMessageId, timestamp);
918 break;
919 case "retract":
920 receiveRetract(from, serverMessageId, timestamp);
921 break;
922 case "reject":
923 receiveReject(from, serverMessageId, timestamp);
924 break;
925 case "accept":
926 receiveAccept(from, serverMessageId, timestamp);
927 break;
928 default:
929 break;
930 }
931 }
932
933 void deliverFailedProceed() {
934 Log.d(
935 Config.LOGTAG,
936 id.account.getJid().asBareJid() + ": receive message error for proceed message");
937 if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) {
938 webRTCWrapper.close();
939 Log.d(
940 Config.LOGTAG,
941 id.account.getJid().asBareJid() + ": transitioned into connectivity error");
942 this.finish();
943 }
944 }
945
946 private void receiveAccept(final Jid from, final String serverMsgId, final long timestamp) {
947 final boolean originatedFromMyself =
948 from.asBareJid().equals(id.account.getJid().asBareJid());
949 if (originatedFromMyself) {
950 if (transition(State.ACCEPTED)) {
951 if (serverMsgId != null) {
952 this.message.setServerMsgId(serverMsgId);
953 }
954 this.message.setTime(timestamp);
955 this.message.setCarbon(true); // indicate that call was accepted on other device
956 this.writeLogMessageSuccess(0);
957 this.xmppConnectionService
958 .getNotificationService()
959 .cancelIncomingCallNotification();
960 this.finish();
961 } else {
962 Log.d(
963 Config.LOGTAG,
964 id.account.getJid().asBareJid()
965 + ": unable to transition to accept because already in state="
966 + this.state);
967 }
968 } else {
969 Log.d(
970 Config.LOGTAG,
971 id.account.getJid().asBareJid() + ": ignoring 'accept' from " + from);
972 }
973 }
974
975 private void receiveReject(final Jid from, final String serverMsgId, final long timestamp) {
976 final boolean originatedFromMyself =
977 from.asBareJid().equals(id.account.getJid().asBareJid());
978 // reject from another one of my clients
979 if (originatedFromMyself) {
980 receiveRejectFromMyself(serverMsgId, timestamp);
981 } else if (isInitiator()) {
982 if (from.equals(id.with)) {
983 receiveRejectFromResponder();
984 } else {
985 Log.d(
986 Config.LOGTAG,
987 id.account.getJid()
988 + ": ignoring reject from "
989 + from
990 + " for session with "
991 + id.with);
992 }
993 } else {
994 Log.d(
995 Config.LOGTAG,
996 id.account.getJid()
997 + ": ignoring reject from "
998 + from
999 + " for session with "
1000 + id.with);
1001 }
1002 }
1003
1004 private void receiveRejectFromMyself(String serverMsgId, long timestamp) {
1005 if (transition(State.REJECTED)) {
1006 this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1007 this.finish();
1008 if (serverMsgId != null) {
1009 this.message.setServerMsgId(serverMsgId);
1010 }
1011 this.message.setTime(timestamp);
1012 this.message.setCarbon(true); // indicate that call was rejected on other device
1013 writeLogMessageMissed();
1014 } else {
1015 Log.d(
1016 Config.LOGTAG,
1017 "not able to transition into REJECTED because already in " + this.state);
1018 }
1019 }
1020
1021 private void receiveRejectFromResponder() {
1022 if (isInState(State.PROCEED)) {
1023 Log.d(
1024 Config.LOGTAG,
1025 id.account.getJid()
1026 + ": received reject while still in proceed. callee reconsidered");
1027 closeTransitionLogFinish(State.REJECTED_RACED);
1028 return;
1029 }
1030 if (isInState(State.SESSION_INITIALIZED_PRE_APPROVED)) {
1031 Log.d(
1032 Config.LOGTAG,
1033 id.account.getJid()
1034 + ": received reject while in SESSION_INITIATED_PRE_APPROVED. callee reconsidered before receiving session-init");
1035 closeTransitionLogFinish(State.TERMINATED_DECLINED_OR_BUSY);
1036 return;
1037 }
1038 Log.d(
1039 Config.LOGTAG,
1040 id.account.getJid()
1041 + ": ignoring reject from responder because already in state "
1042 + this.state);
1043 }
1044
1045 private void receivePropose(
1046 final Jid from, final Propose propose, final String serverMsgId, final long timestamp) {
1047 final boolean originatedFromMyself =
1048 from.asBareJid().equals(id.account.getJid().asBareJid());
1049 if (originatedFromMyself) {
1050 Log.d(
1051 Config.LOGTAG,
1052 id.account.getJid().asBareJid() + ": saw proposal from myself. ignoring");
1053 } else if (transition(
1054 State.PROPOSED,
1055 () -> {
1056 final Collection<RtpDescription> descriptions =
1057 Collections2.transform(
1058 Collections2.filter(
1059 propose.getDescriptions(),
1060 d -> d instanceof RtpDescription),
1061 input -> (RtpDescription) input);
1062 final Collection<Media> media =
1063 Collections2.transform(descriptions, RtpDescription::getMedia);
1064 Preconditions.checkState(
1065 !media.contains(Media.UNKNOWN),
1066 "RTP descriptions contain unknown media");
1067 Log.d(
1068 Config.LOGTAG,
1069 id.account.getJid().asBareJid()
1070 + ": received session proposal from "
1071 + from
1072 + " for "
1073 + media);
1074 this.proposedMedia = Sets.newHashSet(media);
1075 })) {
1076 if (serverMsgId != null) {
1077 this.message.setServerMsgId(serverMsgId);
1078 }
1079 this.message.setTime(timestamp);
1080 startRinging();
1081 } else {
1082 Log.d(
1083 Config.LOGTAG,
1084 id.account.getJid()
1085 + ": ignoring session proposal because already in "
1086 + state);
1087 }
1088 }
1089
1090 private void startRinging() {
1091 Log.d(
1092 Config.LOGTAG,
1093 id.account.getJid().asBareJid()
1094 + ": received call from "
1095 + id.with
1096 + ". start ringing");
1097 ringingTimeoutFuture =
1098 jingleConnectionManager.schedule(
1099 this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS);
1100 xmppConnectionService.getNotificationService().startRinging(id, getMedia());
1101 }
1102
1103 private synchronized void ringingTimeout() {
1104 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": timeout reached for ringing");
1105 switch (this.state) {
1106 case PROPOSED:
1107 message.markUnread();
1108 rejectCallFromProposed();
1109 break;
1110 case SESSION_INITIALIZED:
1111 message.markUnread();
1112 rejectCallFromSessionInitiate();
1113 break;
1114 }
1115 }
1116
1117 private void cancelRingingTimeout() {
1118 final ScheduledFuture<?> future = this.ringingTimeoutFuture;
1119 if (future != null && !future.isCancelled()) {
1120 future.cancel(false);
1121 }
1122 }
1123
1124 private void receiveProceed(
1125 final Jid from, final Proceed proceed, final String serverMsgId, final long timestamp) {
1126 final Set<Media> media =
1127 Preconditions.checkNotNull(
1128 this.proposedMedia, "Proposed media has to be set before handling proceed");
1129 Preconditions.checkState(media.size() > 0, "Proposed media should not be empty");
1130 if (from.equals(id.with)) {
1131 if (isInitiator()) {
1132 if (transition(State.PROCEED)) {
1133 if (serverMsgId != null) {
1134 this.message.setServerMsgId(serverMsgId);
1135 }
1136 this.message.setTime(timestamp);
1137 final Integer remoteDeviceId = proceed.getDeviceId();
1138 if (isOmemoEnabled()) {
1139 this.omemoVerification.setDeviceId(remoteDeviceId);
1140 } else {
1141 if (remoteDeviceId != null) {
1142 Log.d(
1143 Config.LOGTAG,
1144 id.account.getJid().asBareJid()
1145 + ": remote party signaled support for OMEMO verification but we have OMEMO disabled");
1146 }
1147 this.omemoVerification.setDeviceId(null);
1148 }
1149 this.sendSessionInitiate(media, State.SESSION_INITIALIZED_PRE_APPROVED);
1150 } else {
1151 Log.d(
1152 Config.LOGTAG,
1153 String.format(
1154 "%s: ignoring proceed because already in %s",
1155 id.account.getJid().asBareJid(), this.state));
1156 }
1157 } else {
1158 Log.d(
1159 Config.LOGTAG,
1160 String.format(
1161 "%s: ignoring proceed because we were not initializing",
1162 id.account.getJid().asBareJid()));
1163 }
1164 } else if (from.asBareJid().equals(id.account.getJid().asBareJid())) {
1165 if (transition(State.ACCEPTED)) {
1166 Log.d(
1167 Config.LOGTAG,
1168 id.account.getJid().asBareJid()
1169 + ": moved session with "
1170 + id.with
1171 + " into state accepted after received carbon copied procced");
1172 this.xmppConnectionService
1173 .getNotificationService()
1174 .cancelIncomingCallNotification();
1175 this.finish();
1176 }
1177 } else {
1178 Log.d(
1179 Config.LOGTAG,
1180 String.format(
1181 "%s: ignoring proceed from %s. was expected from %s",
1182 id.account.getJid().asBareJid(), from, id.with));
1183 }
1184 }
1185
1186 private void receiveRetract(final Jid from, final String serverMsgId, final long timestamp) {
1187 if (from.equals(id.with)) {
1188 final State target =
1189 this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED;
1190 if (transition(target)) {
1191 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1192 Log.d(
1193 Config.LOGTAG,
1194 id.account.getJid().asBareJid()
1195 + ": session with "
1196 + id.with
1197 + " has been retracted (serverMsgId="
1198 + serverMsgId
1199 + ")");
1200 if (serverMsgId != null) {
1201 this.message.setServerMsgId(serverMsgId);
1202 }
1203 this.message.setTime(timestamp);
1204 if (target == State.RETRACTED) {
1205 this.message.markUnread();
1206 }
1207 writeLogMessageMissed();
1208 finish();
1209 } else {
1210 Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state);
1211 }
1212 } else {
1213 // TODO parse retract from self
1214 Log.d(
1215 Config.LOGTAG,
1216 id.account.getJid().asBareJid()
1217 + ": received retract from "
1218 + from
1219 + ". expected retract from"
1220 + id.with
1221 + ". ignoring");
1222 }
1223 }
1224
1225 public void sendSessionInitiate() {
1226 sendSessionInitiate(this.proposedMedia, State.SESSION_INITIALIZED);
1227 }
1228
1229 private void sendSessionInitiate(final Set<Media> media, final State targetState) {
1230 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
1231 discoverIceServers(iceServers -> sendSessionInitiate(media, targetState, iceServers));
1232 }
1233
1234 private synchronized void sendSessionInitiate(
1235 final Set<Media> media,
1236 final State targetState,
1237 final List<PeerConnection.IceServer> iceServers) {
1238 if (isTerminated()) {
1239 Log.w(
1240 Config.LOGTAG,
1241 id.account.getJid().asBareJid()
1242 + ": ICE servers got discovered when session was already terminated. nothing to do.");
1243 return;
1244 }
1245 try {
1246 setupWebRTC(media, iceServers);
1247 } catch (final WebRTCWrapper.InitializationException e) {
1248 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC");
1249 webRTCWrapper.close();
1250 sendRetract(Reason.ofThrowable(e));
1251 return;
1252 }
1253 try {
1254 org.webrtc.SessionDescription webRTCSessionDescription =
1255 this.webRTCWrapper.setLocalDescription().get();
1256 prepareSessionInitiate(webRTCSessionDescription, targetState);
1257 } catch (final Exception e) {
1258 // TODO sending the error text is worthwhile as well. Especially for FailureToSet
1259 // exceptions
1260 failureToInitiateSession(e, targetState);
1261 }
1262 }
1263
1264 private void failureToInitiateSession(final Throwable throwable, final State targetState) {
1265 if (isTerminated()) {
1266 return;
1267 }
1268 Log.d(
1269 Config.LOGTAG,
1270 id.account.getJid().asBareJid() + ": unable to sendSessionInitiate",
1271 Throwables.getRootCause(throwable));
1272 webRTCWrapper.close();
1273 final Reason reason = Reason.ofThrowable(throwable);
1274 if (isInState(targetState)) {
1275 sendSessionTerminate(reason);
1276 } else {
1277 sendRetract(reason);
1278 }
1279 }
1280
1281 private void sendRetract(final Reason reason) {
1282 // TODO embed reason into retract
1283 sendJingleMessage("retract", id.with.asBareJid());
1284 transitionOrThrow(reasonToState(reason));
1285 this.finish();
1286 }
1287
1288 private void prepareSessionInitiate(
1289 final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) {
1290 final SessionDescription sessionDescription =
1291 SessionDescription.parse(webRTCSessionDescription.description);
1292 final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
1293 this.initiatorRtpContentMap = rtpContentMap;
1294 this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
1295 final ListenableFuture<RtpContentMap> outgoingContentMapFuture =
1296 encryptSessionInitiate(rtpContentMap);
1297 Futures.addCallback(
1298 outgoingContentMapFuture,
1299 new FutureCallback<RtpContentMap>() {
1300 @Override
1301 public void onSuccess(final RtpContentMap outgoingContentMap) {
1302 sendSessionInitiate(outgoingContentMap, targetState);
1303 }
1304
1305 @Override
1306 public void onFailure(@NonNull final Throwable throwable) {
1307 failureToInitiateSession(throwable, targetState);
1308 }
1309 },
1310 MoreExecutors.directExecutor());
1311 }
1312
1313 private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) {
1314 if (isTerminated()) {
1315 Log.w(
1316 Config.LOGTAG,
1317 id.account.getJid().asBareJid()
1318 + ": preparing session was too slow. already terminated. nothing to do.");
1319 return;
1320 }
1321 this.transitionOrThrow(targetState);
1322 final JinglePacket sessionInitiate =
1323 rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
1324 send(sessionInitiate);
1325 }
1326
1327 private ListenableFuture<RtpContentMap> encryptSessionInitiate(
1328 final RtpContentMap rtpContentMap) {
1329 if (this.omemoVerification.hasDeviceId()) {
1330 final ListenableFuture<AxolotlService.OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>>
1331 verifiedPayloadFuture =
1332 id.account
1333 .getAxolotlService()
1334 .encrypt(
1335 rtpContentMap,
1336 id.with,
1337 omemoVerification.getDeviceId());
1338 final ListenableFuture<RtpContentMap> future =
1339 Futures.transform(
1340 verifiedPayloadFuture,
1341 verifiedPayload -> {
1342 omemoVerification.setSessionFingerprint(
1343 verifiedPayload.getFingerprint());
1344 return verifiedPayload.getPayload();
1345 },
1346 MoreExecutors.directExecutor());
1347 if (Config.REQUIRE_RTP_VERIFICATION) {
1348 return future;
1349 }
1350 return Futures.catching(
1351 future,
1352 CryptoFailedException.class,
1353 e -> {
1354 Log.w(
1355 Config.LOGTAG,
1356 id.account.getJid().asBareJid()
1357 + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back",
1358 e);
1359 return rtpContentMap;
1360 },
1361 MoreExecutors.directExecutor());
1362 } else {
1363 return Futures.immediateFuture(rtpContentMap);
1364 }
1365 }
1366
1367 private void sendSessionTerminate(final Reason reason) {
1368 sendSessionTerminate(reason, null);
1369 }
1370
1371 private void sendSessionTerminate(final Reason reason, final String text) {
1372 final State previous = this.state;
1373 final State target = reasonToState(reason);
1374 transitionOrThrow(target);
1375 if (previous != State.NULL) {
1376 writeLogMessage(target);
1377 }
1378 final JinglePacket jinglePacket =
1379 new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
1380 jinglePacket.setReason(reason, text);
1381 Log.d(Config.LOGTAG, jinglePacket.toString());
1382 send(jinglePacket);
1383 finish();
1384 }
1385
1386 private void sendTransportInfo(
1387 final String contentName, IceUdpTransportInfo.Candidate candidate) {
1388 final RtpContentMap transportInfo;
1389 try {
1390 final RtpContentMap rtpContentMap =
1391 isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
1392 transportInfo = rtpContentMap.transportInfo(contentName, candidate);
1393 } catch (final Exception e) {
1394 Log.d(
1395 Config.LOGTAG,
1396 id.account.getJid().asBareJid()
1397 + ": unable to prepare transport-info from candidate for content="
1398 + contentName);
1399 return;
1400 }
1401 final JinglePacket jinglePacket =
1402 transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
1403 send(jinglePacket);
1404 }
1405
1406 private void send(final JinglePacket jinglePacket) {
1407 jinglePacket.setTo(id.with);
1408 xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
1409 }
1410
1411 private synchronized void handleIqResponse(final Account account, final IqPacket response) {
1412 if (response.getType() == IqPacket.TYPE.ERROR) {
1413 handleIqErrorResponse(response);
1414 return;
1415 }
1416 if (response.getType() == IqPacket.TYPE.TIMEOUT) {
1417 handleIqTimeoutResponse(response);
1418 }
1419 }
1420
1421 private void handleIqErrorResponse(final IqPacket response) {
1422 Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
1423 final String errorCondition = response.getErrorCondition();
1424 Log.d(
1425 Config.LOGTAG,
1426 id.account.getJid().asBareJid()
1427 + ": received IQ-error from "
1428 + response.getFrom()
1429 + " in RTP session. "
1430 + errorCondition);
1431 if (isTerminated()) {
1432 Log.i(
1433 Config.LOGTAG,
1434 id.account.getJid().asBareJid()
1435 + ": ignoring error because session was already terminated");
1436 return;
1437 }
1438 this.webRTCWrapper.close();
1439 final State target;
1440 if (Arrays.asList(
1441 "service-unavailable",
1442 "recipient-unavailable",
1443 "remote-server-not-found",
1444 "remote-server-timeout")
1445 .contains(errorCondition)) {
1446 target = State.TERMINATED_CONNECTIVITY_ERROR;
1447 } else {
1448 target = State.TERMINATED_APPLICATION_FAILURE;
1449 }
1450 transitionOrThrow(target);
1451 this.finish();
1452 }
1453
1454 private void handleIqTimeoutResponse(final IqPacket response) {
1455 Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
1456 Log.d(
1457 Config.LOGTAG,
1458 id.account.getJid().asBareJid()
1459 + ": received IQ timeout in RTP session with "
1460 + id.with
1461 + ". terminating with connectivity error");
1462 if (isTerminated()) {
1463 Log.i(
1464 Config.LOGTAG,
1465 id.account.getJid().asBareJid()
1466 + ": ignoring error because session was already terminated");
1467 return;
1468 }
1469 this.webRTCWrapper.close();
1470 transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
1471 this.finish();
1472 }
1473
1474 private void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
1475 Log.d(
1476 Config.LOGTAG,
1477 id.account.getJid().asBareJid() + ": terminating session with out-of-order");
1478 this.webRTCWrapper.close();
1479 transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
1480 respondWithOutOfOrder(jinglePacket);
1481 this.finish();
1482 }
1483
1484 private void respondWithTieBreak(final JinglePacket jinglePacket) {
1485 respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
1486 }
1487
1488 private void respondWithOutOfOrder(final JinglePacket jinglePacket) {
1489 respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
1490 }
1491
1492 void respondWithJingleError(
1493 final IqPacket original,
1494 String jingleCondition,
1495 String condition,
1496 String conditionType) {
1497 jingleConnectionManager.respondWithJingleError(
1498 id.account, original, jingleCondition, condition, conditionType);
1499 }
1500
1501 private void respondOk(final JinglePacket jinglePacket) {
1502 xmppConnectionService.sendIqPacket(
1503 id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
1504 }
1505
1506 public RtpEndUserState getEndUserState() {
1507 switch (this.state) {
1508 case NULL:
1509 case PROPOSED:
1510 case SESSION_INITIALIZED:
1511 if (isInitiator()) {
1512 return RtpEndUserState.RINGING;
1513 } else {
1514 return RtpEndUserState.INCOMING_CALL;
1515 }
1516 case PROCEED:
1517 if (isInitiator()) {
1518 return RtpEndUserState.RINGING;
1519 } else {
1520 return RtpEndUserState.ACCEPTING_CALL;
1521 }
1522 case SESSION_INITIALIZED_PRE_APPROVED:
1523 if (isInitiator()) {
1524 return RtpEndUserState.RINGING;
1525 } else {
1526 return RtpEndUserState.CONNECTING;
1527 }
1528 case SESSION_ACCEPTED:
1529 return getPeerConnectionStateAsEndUserState();
1530 case REJECTED:
1531 case REJECTED_RACED:
1532 case TERMINATED_DECLINED_OR_BUSY:
1533 if (isInitiator()) {
1534 return RtpEndUserState.DECLINED_OR_BUSY;
1535 } else {
1536 return RtpEndUserState.ENDED;
1537 }
1538 case TERMINATED_SUCCESS:
1539 case ACCEPTED:
1540 case RETRACTED:
1541 case TERMINATED_CANCEL_OR_TIMEOUT:
1542 return RtpEndUserState.ENDED;
1543 case RETRACTED_RACED:
1544 if (isInitiator()) {
1545 return RtpEndUserState.ENDED;
1546 } else {
1547 return RtpEndUserState.RETRACTED;
1548 }
1549 case TERMINATED_CONNECTIVITY_ERROR:
1550 return zeroDuration()
1551 ? RtpEndUserState.CONNECTIVITY_ERROR
1552 : RtpEndUserState.CONNECTIVITY_LOST_ERROR;
1553 case TERMINATED_APPLICATION_FAILURE:
1554 return RtpEndUserState.APPLICATION_ERROR;
1555 case TERMINATED_SECURITY_ERROR:
1556 return RtpEndUserState.SECURITY_ERROR;
1557 }
1558 throw new IllegalStateException(
1559 String.format("%s has no equivalent EndUserState", this.state));
1560 }
1561
1562 private RtpEndUserState getPeerConnectionStateAsEndUserState() {
1563 final PeerConnection.PeerConnectionState state;
1564 try {
1565 state = webRTCWrapper.getState();
1566 } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
1567 // We usually close the WebRTCWrapper *before* transitioning so we might still
1568 // be in SESSION_ACCEPTED even though the peerConnection has been torn down
1569 return RtpEndUserState.ENDING_CALL;
1570 }
1571 switch (state) {
1572 case CONNECTED:
1573 return RtpEndUserState.CONNECTED;
1574 case NEW:
1575 case CONNECTING:
1576 return RtpEndUserState.CONNECTING;
1577 case CLOSED:
1578 return RtpEndUserState.ENDING_CALL;
1579 default:
1580 return zeroDuration()
1581 ? RtpEndUserState.CONNECTIVITY_ERROR
1582 : RtpEndUserState.RECONNECTING;
1583 }
1584 }
1585
1586 public Set<Media> getMedia() {
1587 final State current = getState();
1588 if (current == State.NULL) {
1589 if (isInitiator()) {
1590 return Preconditions.checkNotNull(
1591 this.proposedMedia, "RTP connection has not been initialized properly");
1592 }
1593 throw new IllegalStateException("RTP connection has not been initialized yet");
1594 }
1595 if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) {
1596 return Preconditions.checkNotNull(
1597 this.proposedMedia, "RTP connection has not been initialized properly");
1598 }
1599 final RtpContentMap initiatorContentMap = initiatorRtpContentMap;
1600 if (initiatorContentMap != null) {
1601 return initiatorContentMap.getMedia();
1602 } else if (isTerminated()) {
1603 return Collections.emptySet(); // we might fail before we ever got a chance to set media
1604 } else {
1605 return Preconditions.checkNotNull(
1606 this.proposedMedia, "RTP connection has not been initialized properly");
1607 }
1608 }
1609
1610 public boolean isVerified() {
1611 final String fingerprint = this.omemoVerification.getFingerprint();
1612 if (fingerprint == null) {
1613 return false;
1614 }
1615 final FingerprintStatus status =
1616 id.account.getAxolotlService().getFingerprintTrust(fingerprint);
1617 return status != null && status.isVerified();
1618 }
1619
1620 public synchronized void acceptCall() {
1621 switch (this.state) {
1622 case PROPOSED:
1623 cancelRingingTimeout();
1624 acceptCallFromProposed();
1625 break;
1626 case SESSION_INITIALIZED:
1627 cancelRingingTimeout();
1628 acceptCallFromSessionInitialized();
1629 break;
1630 case ACCEPTED:
1631 Log.w(
1632 Config.LOGTAG,
1633 id.account.getJid().asBareJid()
1634 + ": the call has already been accepted with another client. UI was just lagging behind");
1635 break;
1636 case PROCEED:
1637 case SESSION_ACCEPTED:
1638 Log.w(
1639 Config.LOGTAG,
1640 id.account.getJid().asBareJid()
1641 + ": the call has already been accepted. user probably double tapped the UI");
1642 break;
1643 default:
1644 throw new IllegalStateException("Can not accept call from " + this.state);
1645 }
1646 }
1647
1648 public void notifyPhoneCall() {
1649 Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections");
1650 if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) {
1651 rejectCall();
1652 } else {
1653 endCall();
1654 }
1655 }
1656
1657 public synchronized void rejectCall() {
1658 if (isTerminated()) {
1659 Log.w(
1660 Config.LOGTAG,
1661 id.account.getJid().asBareJid()
1662 + ": received rejectCall() when session has already been terminated. nothing to do");
1663 return;
1664 }
1665 switch (this.state) {
1666 case PROPOSED:
1667 rejectCallFromProposed();
1668 break;
1669 case SESSION_INITIALIZED:
1670 rejectCallFromSessionInitiate();
1671 break;
1672 default:
1673 throw new IllegalStateException("Can not reject call from " + this.state);
1674 }
1675 }
1676
1677 public synchronized void endCall() {
1678 if (isTerminated()) {
1679 Log.w(
1680 Config.LOGTAG,
1681 id.account.getJid().asBareJid()
1682 + ": received endCall() when session has already been terminated. nothing to do");
1683 return;
1684 }
1685 if (isInState(State.PROPOSED) && !isInitiator()) {
1686 rejectCallFromProposed();
1687 return;
1688 }
1689 if (isInState(State.PROCEED)) {
1690 if (isInitiator()) {
1691 retractFromProceed();
1692 } else {
1693 rejectCallFromProceed();
1694 }
1695 return;
1696 }
1697 if (isInitiator()
1698 && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) {
1699 this.webRTCWrapper.close();
1700 sendSessionTerminate(Reason.CANCEL);
1701 return;
1702 }
1703 if (isInState(State.SESSION_INITIALIZED)) {
1704 rejectCallFromSessionInitiate();
1705 return;
1706 }
1707 if (isInState(State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
1708 this.webRTCWrapper.close();
1709 sendSessionTerminate(Reason.SUCCESS);
1710 return;
1711 }
1712 if (isInState(
1713 State.TERMINATED_APPLICATION_FAILURE,
1714 State.TERMINATED_CONNECTIVITY_ERROR,
1715 State.TERMINATED_DECLINED_OR_BUSY)) {
1716 Log.d(
1717 Config.LOGTAG,
1718 "ignoring request to end call because already in state " + this.state);
1719 return;
1720 }
1721 throw new IllegalStateException(
1722 "called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator());
1723 }
1724
1725 private void retractFromProceed() {
1726 Log.d(Config.LOGTAG, "retract from proceed");
1727 this.sendJingleMessage("retract");
1728 closeTransitionLogFinish(State.RETRACTED_RACED);
1729 }
1730
1731 private void closeTransitionLogFinish(final State state) {
1732 this.webRTCWrapper.close();
1733 transitionOrThrow(state);
1734 writeLogMessage(state);
1735 finish();
1736 }
1737
1738 private void setupWebRTC(
1739 final Set<Media> media, final List<PeerConnection.IceServer> iceServers)
1740 throws WebRTCWrapper.InitializationException {
1741 this.jingleConnectionManager.ensureConnectionIsRegistered(this);
1742 final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference;
1743 if (media.contains(Media.VIDEO)) {
1744 speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.SPEAKER;
1745 } else {
1746 speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.EARPIECE;
1747 }
1748 this.webRTCWrapper.setup(this.xmppConnectionService, speakerPhonePreference);
1749 this.webRTCWrapper.initializePeerConnection(media, iceServers);
1750 }
1751
1752 private void acceptCallFromProposed() {
1753 transitionOrThrow(State.PROCEED);
1754 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1755 this.sendJingleMessage("accept", id.account.getJid().asBareJid());
1756 this.sendJingleMessage("proceed");
1757 }
1758
1759 private void rejectCallFromProposed() {
1760 transitionOrThrow(State.REJECTED);
1761 writeLogMessageMissed();
1762 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1763 this.sendJingleMessage("reject");
1764 finish();
1765 }
1766
1767 private void rejectCallFromProceed() {
1768 this.sendJingleMessage("reject");
1769 closeTransitionLogFinish(State.REJECTED_RACED);
1770 }
1771
1772 private void rejectCallFromSessionInitiate() {
1773 webRTCWrapper.close();
1774 sendSessionTerminate(Reason.DECLINE);
1775 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1776 }
1777
1778 private void sendJingleMessage(final String action) {
1779 sendJingleMessage(action, id.with);
1780 }
1781
1782 private void sendJingleMessage(final String action, final Jid to) {
1783 final MessagePacket messagePacket = new MessagePacket();
1784 messagePacket.setType(MessagePacket.TYPE_CHAT); // we want to carbon copy those
1785 messagePacket.setTo(to);
1786 final Element intent =
1787 messagePacket
1788 .addChild(action, Namespace.JINGLE_MESSAGE)
1789 .setAttribute("id", id.sessionId);
1790 if ("proceed".equals(action)) {
1791 messagePacket.setId(JINGLE_MESSAGE_PROCEED_ID_PREFIX + id.sessionId);
1792 if (isOmemoEnabled()) {
1793 final int deviceId = id.account.getAxolotlService().getOwnDeviceId();
1794 final Element device =
1795 intent.addChild("device", Namespace.OMEMO_DTLS_SRTP_VERIFICATION);
1796 device.setAttribute("id", deviceId);
1797 }
1798 }
1799 messagePacket.addChild("store", "urn:xmpp:hints");
1800 xmppConnectionService.sendMessagePacket(id.account, messagePacket);
1801 }
1802
1803 private boolean isOmemoEnabled() {
1804 final Conversational conversational = message.getConversation();
1805 if (conversational instanceof Conversation) {
1806 return ((Conversation) conversational).getNextEncryption()
1807 == Message.ENCRYPTION_AXOLOTL;
1808 }
1809 return false;
1810 }
1811
1812 private void acceptCallFromSessionInitialized() {
1813 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1814 sendSessionAccept();
1815 }
1816
1817 private synchronized boolean isInState(State... state) {
1818 return Arrays.asList(state).contains(this.state);
1819 }
1820
1821 private boolean transition(final State target) {
1822 return transition(target, null);
1823 }
1824
1825 private synchronized boolean transition(final State target, final Runnable runnable) {
1826 final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
1827 if (validTransitions != null && validTransitions.contains(target)) {
1828 this.state = target;
1829 if (runnable != null) {
1830 runnable.run();
1831 }
1832 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
1833 updateEndUserState();
1834 updateOngoingCallNotification();
1835 return true;
1836 } else {
1837 return false;
1838 }
1839 }
1840
1841 void transitionOrThrow(final State target) {
1842 if (!transition(target)) {
1843 throw new IllegalStateException(
1844 String.format("Unable to transition from %s to %s", this.state, target));
1845 }
1846 }
1847
1848 @Override
1849 public void onIceCandidate(final IceCandidate iceCandidate) {
1850 final RtpContentMap rtpContentMap =
1851 isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
1852 final String ufrag = rtpContentMap.getCredentials().ufrag;
1853 final IceUdpTransportInfo.Candidate candidate =
1854 IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag);
1855 if (candidate == null) {
1856 Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate);
1857 return;
1858 }
1859 Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate);
1860 sendTransportInfo(iceCandidate.sdpMid, candidate);
1861 }
1862
1863 @Override
1864 public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
1865 Log.d(
1866 Config.LOGTAG,
1867 id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState);
1868 this.stateHistory.add(newState);
1869 if (newState == PeerConnection.PeerConnectionState.CONNECTED) {
1870 this.sessionDuration.start();
1871 updateOngoingCallNotification();
1872 } else if (this.sessionDuration.isRunning()) {
1873 this.sessionDuration.stop();
1874 updateOngoingCallNotification();
1875 }
1876
1877 final boolean neverConnected =
1878 !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED);
1879
1880 if (newState == PeerConnection.PeerConnectionState.FAILED) {
1881 if (neverConnected) {
1882 if (isTerminated()) {
1883 Log.d(
1884 Config.LOGTAG,
1885 id.account.getJid().asBareJid()
1886 + ": not sending session-terminate after connectivity error because session is already in state "
1887 + this.state);
1888 return;
1889 }
1890 webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection);
1891 return;
1892 } else {
1893 webRTCWrapper.restartIce();
1894 }
1895 }
1896 updateEndUserState();
1897 }
1898
1899 @Override
1900 public void onRenegotiationNeeded() {
1901 this.webRTCWrapper.execute(this::initiateIceRestart);
1902 }
1903
1904 private void initiateIceRestart() {
1905 // TODO discover new TURN/STUN credentials
1906 this.stateHistory.clear();
1907 this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
1908 final SessionDescription sessionDescription;
1909 try {
1910 sessionDescription = setLocalSessionDescription();
1911 } catch (final Exception e) {
1912 final Throwable cause = Throwables.getRootCause(e);
1913 Log.d(Config.LOGTAG, "failed to renegotiate", cause);
1914 sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
1915 return;
1916 }
1917 final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
1918 final RtpContentMap transportInfo = rtpContentMap.transportInfo();
1919 final JinglePacket jinglePacket =
1920 transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
1921 Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket);
1922 jinglePacket.setTo(id.with);
1923 xmppConnectionService.sendIqPacket(
1924 id.account,
1925 jinglePacket,
1926 (account, response) -> {
1927 if (response.getType() == IqPacket.TYPE.RESULT) {
1928 Log.d(Config.LOGTAG, "received success to our ice restart");
1929 setLocalContentMap(rtpContentMap);
1930 webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
1931 return;
1932 }
1933 if (response.getType() == IqPacket.TYPE.ERROR) {
1934 final Element error = response.findChild("error");
1935 if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) {
1936 Log.d(Config.LOGTAG, "received tie-break as result of ice restart");
1937 return;
1938 }
1939 handleIqErrorResponse(response);
1940 }
1941 if (response.getType() == IqPacket.TYPE.TIMEOUT) {
1942 handleIqTimeoutResponse(response);
1943 }
1944 });
1945 }
1946
1947 private void setLocalContentMap(final RtpContentMap rtpContentMap) {
1948 if (isInitiator()) {
1949 this.initiatorRtpContentMap = rtpContentMap;
1950 } else {
1951 this.responderRtpContentMap = rtpContentMap;
1952 }
1953 }
1954
1955 private void setRemoteContentMap(final RtpContentMap rtpContentMap) {
1956 if (isInitiator()) {
1957 this.responderRtpContentMap = rtpContentMap;
1958 } else {
1959 this.initiatorRtpContentMap = rtpContentMap;
1960 }
1961 }
1962
1963 private SessionDescription setLocalSessionDescription()
1964 throws ExecutionException, InterruptedException {
1965 final org.webrtc.SessionDescription sessionDescription =
1966 this.webRTCWrapper.setLocalDescription().get();
1967 return SessionDescription.parse(sessionDescription.description);
1968 }
1969
1970 private void closeWebRTCSessionAfterFailedConnection() {
1971 this.webRTCWrapper.close();
1972 synchronized (this) {
1973 if (isTerminated()) {
1974 Log.d(
1975 Config.LOGTAG,
1976 id.account.getJid().asBareJid()
1977 + ": no need to send session-terminate after failed connection. Other party already did");
1978 return;
1979 }
1980 sendSessionTerminate(Reason.CONNECTIVITY_ERROR);
1981 }
1982 }
1983
1984 public boolean zeroDuration() {
1985 return this.sessionDuration.elapsed(TimeUnit.NANOSECONDS) <= 0;
1986 }
1987
1988 public long getCallDuration() {
1989 return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS);
1990 }
1991
1992 public AppRTCAudioManager getAudioManager() {
1993 return webRTCWrapper.getAudioManager();
1994 }
1995
1996 public boolean isMicrophoneEnabled() {
1997 return webRTCWrapper.isMicrophoneEnabled();
1998 }
1999
2000 public boolean setMicrophoneEnabled(final boolean enabled) {
2001 return webRTCWrapper.setMicrophoneEnabled(enabled);
2002 }
2003
2004 public boolean isVideoEnabled() {
2005 return webRTCWrapper.isVideoEnabled();
2006 }
2007
2008 public void setVideoEnabled(final boolean enabled) {
2009 webRTCWrapper.setVideoEnabled(enabled);
2010 }
2011
2012 public boolean isCameraSwitchable() {
2013 return webRTCWrapper.isCameraSwitchable();
2014 }
2015
2016 public boolean isFrontCamera() {
2017 return webRTCWrapper.isFrontCamera();
2018 }
2019
2020 public ListenableFuture<Boolean> switchCamera() {
2021 return webRTCWrapper.switchCamera();
2022 }
2023
2024 @Override
2025 public void onAudioDeviceChanged(
2026 AppRTCAudioManager.AudioDevice selectedAudioDevice,
2027 Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
2028 xmppConnectionService.notifyJingleRtpConnectionUpdate(
2029 selectedAudioDevice, availableAudioDevices);
2030 }
2031
2032 private void updateEndUserState() {
2033 final RtpEndUserState endUserState = getEndUserState();
2034 jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia());
2035 xmppConnectionService.notifyJingleRtpConnectionUpdate(
2036 id.account, id.with, id.sessionId, endUserState);
2037 }
2038
2039 private void updateOngoingCallNotification() {
2040 final State state = this.state;
2041 if (STATES_SHOWING_ONGOING_CALL.contains(state)) {
2042 final boolean reconnecting;
2043 if (state == State.SESSION_ACCEPTED) {
2044 reconnecting =
2045 getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING;
2046 } else {
2047 reconnecting = false;
2048 }
2049 xmppConnectionService.setOngoingCall(id, getMedia(), reconnecting);
2050 } else {
2051 xmppConnectionService.removeOngoingCall();
2052 }
2053 }
2054
2055 private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) {
2056 if (id.account.getXmppConnection().getFeatures().externalServiceDiscovery()) {
2057 final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2058 request.setTo(id.account.getDomain());
2059 request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
2060 xmppConnectionService.sendIqPacket(
2061 id.account,
2062 request,
2063 (account, response) -> {
2064 ImmutableList.Builder<PeerConnection.IceServer> listBuilder =
2065 new ImmutableList.Builder<>();
2066 if (response.getType() == IqPacket.TYPE.RESULT) {
2067 final Element services =
2068 response.findChild(
2069 "services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
2070 final List<Element> children =
2071 services == null
2072 ? Collections.emptyList()
2073 : services.getChildren();
2074 for (final Element child : children) {
2075 if ("service".equals(child.getName())) {
2076 final String type = child.getAttribute("type");
2077 final String host = child.getAttribute("host");
2078 final String sport = child.getAttribute("port");
2079 final Integer port =
2080 sport == null ? null : Ints.tryParse(sport);
2081 final String transport = child.getAttribute("transport");
2082 final String username = child.getAttribute("username");
2083 final String password = child.getAttribute("password");
2084 if (Strings.isNullOrEmpty(host) || port == null) {
2085 continue;
2086 }
2087 if (port < 0 || port > 65535) {
2088 continue;
2089 }
2090 if (Arrays.asList("stun", "stuns", "turn", "turns")
2091 .contains(type)
2092 && Arrays.asList("udp", "tcp").contains(transport)) {
2093 if (Arrays.asList("stuns", "turns").contains(type)
2094 && "udp".equals(transport)) {
2095 Log.d(
2096 Config.LOGTAG,
2097 id.account.getJid().asBareJid()
2098 + ": skipping invalid combination of udp/tls in external services");
2099 continue;
2100 }
2101 final PeerConnection.IceServer.Builder iceServerBuilder =
2102 PeerConnection.IceServer.builder(
2103 String.format(
2104 "%s:%s:%s?transport=%s",
2105 type,
2106 IP.wrapIPv6(host),
2107 port,
2108 transport));
2109 iceServerBuilder.setTlsCertPolicy(
2110 PeerConnection.TlsCertPolicy
2111 .TLS_CERT_POLICY_INSECURE_NO_CHECK);
2112 if (username != null && password != null) {
2113 iceServerBuilder.setUsername(username);
2114 iceServerBuilder.setPassword(password);
2115 } else if (Arrays.asList("turn", "turns").contains(type)) {
2116 // The WebRTC spec requires throwing an
2117 // InvalidAccessError when username (from libwebrtc
2118 // source coder)
2119 // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
2120 Log.d(
2121 Config.LOGTAG,
2122 id.account.getJid().asBareJid()
2123 + ": skipping "
2124 + type
2125 + "/"
2126 + transport
2127 + " without username and password");
2128 continue;
2129 }
2130 final PeerConnection.IceServer iceServer =
2131 iceServerBuilder.createIceServer();
2132 Log.d(
2133 Config.LOGTAG,
2134 id.account.getJid().asBareJid()
2135 + ": discovered ICE Server: "
2136 + iceServer);
2137 listBuilder.add(iceServer);
2138 }
2139 }
2140 }
2141 }
2142 final List<PeerConnection.IceServer> iceServers = listBuilder.build();
2143 if (iceServers.size() == 0) {
2144 Log.w(
2145 Config.LOGTAG,
2146 id.account.getJid().asBareJid()
2147 + ": no ICE server found "
2148 + response);
2149 }
2150 onIceServersDiscovered.onIceServersDiscovered(iceServers);
2151 });
2152 } else {
2153 Log.w(
2154 Config.LOGTAG,
2155 id.account.getJid().asBareJid() + ": has no external service discovery");
2156 onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList());
2157 }
2158 }
2159
2160 private void finish() {
2161 if (isTerminated()) {
2162 this.cancelRingingTimeout();
2163 this.webRTCWrapper.verifyClosed();
2164 this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
2165 this.jingleConnectionManager.finishConnectionOrThrow(this);
2166 } else {
2167 throw new IllegalStateException(
2168 String.format("Unable to call finish from %s", this.state));
2169 }
2170 }
2171
2172 private void writeLogMessage(final State state) {
2173 final long duration = getCallDuration();
2174 if (state == State.TERMINATED_SUCCESS
2175 || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) {
2176 writeLogMessageSuccess(duration);
2177 } else {
2178 writeLogMessageMissed();
2179 }
2180 }
2181
2182 private void writeLogMessageSuccess(final long duration) {
2183 this.message.setBody(new RtpSessionStatus(true, duration).toString());
2184 this.writeMessage();
2185 }
2186
2187 private void writeLogMessageMissed() {
2188 this.message.setBody(new RtpSessionStatus(false, 0).toString());
2189 this.writeMessage();
2190 }
2191
2192 private void writeMessage() {
2193 final Conversational conversational = message.getConversation();
2194 if (conversational instanceof Conversation) {
2195 ((Conversation) conversational).add(this.message);
2196 xmppConnectionService.createMessageAsync(message);
2197 xmppConnectionService.updateConversationUi();
2198 } else {
2199 throw new IllegalStateException("Somehow the conversation in a message was a stub");
2200 }
2201 }
2202
2203 public State getState() {
2204 return this.state;
2205 }
2206
2207 boolean isTerminated() {
2208 return TERMINATED.contains(this.state);
2209 }
2210
2211 public Optional<VideoTrack> getLocalVideoTrack() {
2212 return webRTCWrapper.getLocalVideoTrack();
2213 }
2214
2215 public Optional<VideoTrack> getRemoteVideoTrack() {
2216 return webRTCWrapper.getRemoteVideoTrack();
2217 }
2218
2219 public EglBase.Context getEglBaseContext() {
2220 return webRTCWrapper.getEglBaseContext();
2221 }
2222
2223 void setProposedMedia(final Set<Media> media) {
2224 this.proposedMedia = media;
2225 }
2226
2227 public void fireStateUpdate() {
2228 final RtpEndUserState endUserState = getEndUserState();
2229 xmppConnectionService.notifyJingleRtpConnectionUpdate(
2230 id.account, id.with, id.sessionId, endUserState);
2231 }
2232
2233 private interface OnIceServersDiscovered {
2234 void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
2235 }
2236}