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