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);
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() {
932 Log.d(
933 Config.LOGTAG,
934 id.account.getJid().asBareJid() + ": receive message error for proceed 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 }
1114
1115 private void cancelRingingTimeout() {
1116 final ScheduledFuture<?> future = this.ringingTimeoutFuture;
1117 if (future != null && !future.isCancelled()) {
1118 future.cancel(false);
1119 }
1120 }
1121
1122 private void receiveProceed(
1123 final Jid from, final Proceed proceed, final String serverMsgId, final long timestamp) {
1124 final Set<Media> media =
1125 Preconditions.checkNotNull(
1126 this.proposedMedia, "Proposed media has to be set before handling proceed");
1127 Preconditions.checkState(media.size() > 0, "Proposed media should not be empty");
1128 if (from.equals(id.with)) {
1129 if (isInitiator()) {
1130 if (transition(State.PROCEED)) {
1131 if (serverMsgId != null) {
1132 this.message.setServerMsgId(serverMsgId);
1133 }
1134 this.message.setTime(timestamp);
1135 final Integer remoteDeviceId = proceed.getDeviceId();
1136 if (isOmemoEnabled()) {
1137 this.omemoVerification.setDeviceId(remoteDeviceId);
1138 } else {
1139 if (remoteDeviceId != null) {
1140 Log.d(
1141 Config.LOGTAG,
1142 id.account.getJid().asBareJid()
1143 + ": remote party signaled support for OMEMO verification but we have OMEMO disabled");
1144 }
1145 this.omemoVerification.setDeviceId(null);
1146 }
1147 this.sendSessionInitiate(media, State.SESSION_INITIALIZED_PRE_APPROVED);
1148 } else {
1149 Log.d(
1150 Config.LOGTAG,
1151 String.format(
1152 "%s: ignoring proceed because already in %s",
1153 id.account.getJid().asBareJid(), this.state));
1154 }
1155 } else {
1156 Log.d(
1157 Config.LOGTAG,
1158 String.format(
1159 "%s: ignoring proceed because we were not initializing",
1160 id.account.getJid().asBareJid()));
1161 }
1162 } else if (from.asBareJid().equals(id.account.getJid().asBareJid())) {
1163 if (transition(State.ACCEPTED)) {
1164 Log.d(
1165 Config.LOGTAG,
1166 id.account.getJid().asBareJid()
1167 + ": moved session with "
1168 + id.with
1169 + " into state accepted after received carbon copied procced");
1170 this.xmppConnectionService
1171 .getNotificationService()
1172 .cancelIncomingCallNotification();
1173 this.finish();
1174 }
1175 } else {
1176 Log.d(
1177 Config.LOGTAG,
1178 String.format(
1179 "%s: ignoring proceed from %s. was expected from %s",
1180 id.account.getJid().asBareJid(), from, id.with));
1181 }
1182 }
1183
1184 private void receiveRetract(final Jid from, final String serverMsgId, final long timestamp) {
1185 if (from.equals(id.with)) {
1186 final State target =
1187 this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED;
1188 if (transition(target)) {
1189 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1190 Log.d(
1191 Config.LOGTAG,
1192 id.account.getJid().asBareJid()
1193 + ": session with "
1194 + id.with
1195 + " has been retracted (serverMsgId="
1196 + serverMsgId
1197 + ")");
1198 if (serverMsgId != null) {
1199 this.message.setServerMsgId(serverMsgId);
1200 }
1201 this.message.setTime(timestamp);
1202 if (target == State.RETRACTED) {
1203 this.message.markUnread();
1204 }
1205 writeLogMessageMissed();
1206 finish();
1207 } else {
1208 Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state);
1209 }
1210 } else {
1211 // TODO parse retract from self
1212 Log.d(
1213 Config.LOGTAG,
1214 id.account.getJid().asBareJid()
1215 + ": received retract from "
1216 + from
1217 + ". expected retract from"
1218 + id.with
1219 + ". ignoring");
1220 }
1221 }
1222
1223 public void sendSessionInitiate() {
1224 sendSessionInitiate(this.proposedMedia, State.SESSION_INITIALIZED);
1225 }
1226
1227 private void sendSessionInitiate(final Set<Media> media, final State targetState) {
1228 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
1229 discoverIceServers(iceServers -> sendSessionInitiate(media, targetState, iceServers));
1230 }
1231
1232 private synchronized void sendSessionInitiate(
1233 final Set<Media> media,
1234 final State targetState,
1235 final List<PeerConnection.IceServer> iceServers) {
1236 if (isTerminated()) {
1237 Log.w(
1238 Config.LOGTAG,
1239 id.account.getJid().asBareJid()
1240 + ": ICE servers got discovered when session was already terminated. nothing to do.");
1241 return;
1242 }
1243 try {
1244 setupWebRTC(media, iceServers);
1245 } catch (final WebRTCWrapper.InitializationException e) {
1246 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC");
1247 webRTCWrapper.close();
1248 sendRetract(Reason.ofThrowable(e));
1249 return;
1250 }
1251 try {
1252 org.webrtc.SessionDescription webRTCSessionDescription =
1253 this.webRTCWrapper.setLocalDescription().get();
1254 prepareSessionInitiate(webRTCSessionDescription, targetState);
1255 } catch (final Exception e) {
1256 // TODO sending the error text is worthwhile as well. Especially for FailureToSet
1257 // exceptions
1258 failureToInitiateSession(e, targetState);
1259 }
1260 }
1261
1262 private void failureToInitiateSession(final Throwable throwable, final State targetState) {
1263 if (isTerminated()) {
1264 return;
1265 }
1266 Log.d(
1267 Config.LOGTAG,
1268 id.account.getJid().asBareJid() + ": unable to sendSessionInitiate",
1269 Throwables.getRootCause(throwable));
1270 webRTCWrapper.close();
1271 final Reason reason = Reason.ofThrowable(throwable);
1272 if (isInState(targetState)) {
1273 sendSessionTerminate(reason);
1274 } else {
1275 sendRetract(reason);
1276 }
1277 }
1278
1279 private void sendRetract(final Reason reason) {
1280 // TODO embed reason into retract
1281 sendJingleMessage("retract", id.with.asBareJid());
1282 transitionOrThrow(reasonToState(reason));
1283 this.finish();
1284 }
1285
1286 private void prepareSessionInitiate(
1287 final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) {
1288 final SessionDescription sessionDescription =
1289 SessionDescription.parse(webRTCSessionDescription.description);
1290 final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
1291 this.initiatorRtpContentMap = rtpContentMap;
1292 this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
1293 final ListenableFuture<RtpContentMap> outgoingContentMapFuture =
1294 encryptSessionInitiate(rtpContentMap);
1295 Futures.addCallback(
1296 outgoingContentMapFuture,
1297 new FutureCallback<RtpContentMap>() {
1298 @Override
1299 public void onSuccess(final RtpContentMap outgoingContentMap) {
1300 sendSessionInitiate(outgoingContentMap, targetState);
1301 }
1302
1303 @Override
1304 public void onFailure(@NonNull final Throwable throwable) {
1305 failureToInitiateSession(throwable, targetState);
1306 }
1307 },
1308 MoreExecutors.directExecutor());
1309 }
1310
1311 private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) {
1312 if (isTerminated()) {
1313 Log.w(
1314 Config.LOGTAG,
1315 id.account.getJid().asBareJid()
1316 + ": preparing session was too slow. already terminated. nothing to do.");
1317 return;
1318 }
1319 this.transitionOrThrow(targetState);
1320 final JinglePacket sessionInitiate =
1321 rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
1322 send(sessionInitiate);
1323 }
1324
1325 private ListenableFuture<RtpContentMap> encryptSessionInitiate(
1326 final RtpContentMap rtpContentMap) {
1327 if (this.omemoVerification.hasDeviceId()) {
1328 final ListenableFuture<AxolotlService.OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>>
1329 verifiedPayloadFuture =
1330 id.account
1331 .getAxolotlService()
1332 .encrypt(
1333 rtpContentMap,
1334 id.with,
1335 omemoVerification.getDeviceId());
1336 final ListenableFuture<RtpContentMap> future =
1337 Futures.transform(
1338 verifiedPayloadFuture,
1339 verifiedPayload -> {
1340 omemoVerification.setSessionFingerprint(
1341 verifiedPayload.getFingerprint());
1342 return verifiedPayload.getPayload();
1343 },
1344 MoreExecutors.directExecutor());
1345 if (Config.REQUIRE_RTP_VERIFICATION) {
1346 return future;
1347 }
1348 return Futures.catching(
1349 future,
1350 CryptoFailedException.class,
1351 e -> {
1352 Log.w(
1353 Config.LOGTAG,
1354 id.account.getJid().asBareJid()
1355 + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back",
1356 e);
1357 return rtpContentMap;
1358 },
1359 MoreExecutors.directExecutor());
1360 } else {
1361 return Futures.immediateFuture(rtpContentMap);
1362 }
1363 }
1364
1365 private void sendSessionTerminate(final Reason reason) {
1366 sendSessionTerminate(reason, null);
1367 }
1368
1369 private void sendSessionTerminate(final Reason reason, final String text) {
1370 final State previous = this.state;
1371 final State target = reasonToState(reason);
1372 transitionOrThrow(target);
1373 if (previous != State.NULL) {
1374 writeLogMessage(target);
1375 }
1376 final JinglePacket jinglePacket =
1377 new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
1378 jinglePacket.setReason(reason, text);
1379 Log.d(Config.LOGTAG, jinglePacket.toString());
1380 send(jinglePacket);
1381 finish();
1382 }
1383
1384 private void sendTransportInfo(
1385 final String contentName, IceUdpTransportInfo.Candidate candidate) {
1386 final RtpContentMap transportInfo;
1387 try {
1388 final RtpContentMap rtpContentMap =
1389 isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
1390 transportInfo = rtpContentMap.transportInfo(contentName, candidate);
1391 } catch (final Exception e) {
1392 Log.d(
1393 Config.LOGTAG,
1394 id.account.getJid().asBareJid()
1395 + ": unable to prepare transport-info from candidate for content="
1396 + contentName);
1397 return;
1398 }
1399 final JinglePacket jinglePacket =
1400 transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
1401 send(jinglePacket);
1402 }
1403
1404 private void send(final JinglePacket jinglePacket) {
1405 jinglePacket.setTo(id.with);
1406 xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
1407 }
1408
1409 private synchronized void handleIqResponse(final Account account, final IqPacket response) {
1410 if (response.getType() == IqPacket.TYPE.ERROR) {
1411 handleIqErrorResponse(response);
1412 return;
1413 }
1414 if (response.getType() == IqPacket.TYPE.TIMEOUT) {
1415 handleIqTimeoutResponse(response);
1416 }
1417 }
1418
1419 private void handleIqErrorResponse(final IqPacket response) {
1420 Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
1421 final String errorCondition = response.getErrorCondition();
1422 Log.d(
1423 Config.LOGTAG,
1424 id.account.getJid().asBareJid()
1425 + ": received IQ-error from "
1426 + response.getFrom()
1427 + " in RTP session. "
1428 + errorCondition);
1429 if (isTerminated()) {
1430 Log.i(
1431 Config.LOGTAG,
1432 id.account.getJid().asBareJid()
1433 + ": ignoring error because session was already terminated");
1434 return;
1435 }
1436 this.webRTCWrapper.close();
1437 final State target;
1438 if (Arrays.asList(
1439 "service-unavailable",
1440 "recipient-unavailable",
1441 "remote-server-not-found",
1442 "remote-server-timeout")
1443 .contains(errorCondition)) {
1444 target = State.TERMINATED_CONNECTIVITY_ERROR;
1445 } else {
1446 target = State.TERMINATED_APPLICATION_FAILURE;
1447 }
1448 transitionOrThrow(target);
1449 this.finish();
1450 }
1451
1452 private void handleIqTimeoutResponse(final IqPacket response) {
1453 Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
1454 Log.d(
1455 Config.LOGTAG,
1456 id.account.getJid().asBareJid()
1457 + ": received IQ timeout in RTP session with "
1458 + id.with
1459 + ". terminating with connectivity error");
1460 if (isTerminated()) {
1461 Log.i(
1462 Config.LOGTAG,
1463 id.account.getJid().asBareJid()
1464 + ": ignoring error because session was already terminated");
1465 return;
1466 }
1467 this.webRTCWrapper.close();
1468 transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
1469 this.finish();
1470 }
1471
1472 private void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
1473 Log.d(
1474 Config.LOGTAG,
1475 id.account.getJid().asBareJid() + ": terminating session with out-of-order");
1476 this.webRTCWrapper.close();
1477 transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
1478 respondWithOutOfOrder(jinglePacket);
1479 this.finish();
1480 }
1481
1482 private void respondWithTieBreak(final JinglePacket jinglePacket) {
1483 respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
1484 }
1485
1486 private void respondWithOutOfOrder(final JinglePacket jinglePacket) {
1487 respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
1488 }
1489
1490 void respondWithJingleError(
1491 final IqPacket original,
1492 String jingleCondition,
1493 String condition,
1494 String conditionType) {
1495 jingleConnectionManager.respondWithJingleError(
1496 id.account, original, jingleCondition, condition, conditionType);
1497 }
1498
1499 private void respondOk(final JinglePacket jinglePacket) {
1500 xmppConnectionService.sendIqPacket(
1501 id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
1502 }
1503
1504 public RtpEndUserState getEndUserState() {
1505 switch (this.state) {
1506 case NULL:
1507 case PROPOSED:
1508 case SESSION_INITIALIZED:
1509 if (isInitiator()) {
1510 return RtpEndUserState.RINGING;
1511 } else {
1512 return RtpEndUserState.INCOMING_CALL;
1513 }
1514 case PROCEED:
1515 if (isInitiator()) {
1516 return RtpEndUserState.RINGING;
1517 } else {
1518 return RtpEndUserState.ACCEPTING_CALL;
1519 }
1520 case SESSION_INITIALIZED_PRE_APPROVED:
1521 if (isInitiator()) {
1522 return RtpEndUserState.RINGING;
1523 } else {
1524 return RtpEndUserState.CONNECTING;
1525 }
1526 case SESSION_ACCEPTED:
1527 return getPeerConnectionStateAsEndUserState();
1528 case REJECTED:
1529 case REJECTED_RACED:
1530 case TERMINATED_DECLINED_OR_BUSY:
1531 if (isInitiator()) {
1532 return RtpEndUserState.DECLINED_OR_BUSY;
1533 } else {
1534 return RtpEndUserState.ENDED;
1535 }
1536 case TERMINATED_SUCCESS:
1537 case ACCEPTED:
1538 case RETRACTED:
1539 case TERMINATED_CANCEL_OR_TIMEOUT:
1540 return RtpEndUserState.ENDED;
1541 case RETRACTED_RACED:
1542 if (isInitiator()) {
1543 return RtpEndUserState.ENDED;
1544 } else {
1545 return RtpEndUserState.RETRACTED;
1546 }
1547 case TERMINATED_CONNECTIVITY_ERROR:
1548 return zeroDuration()
1549 ? RtpEndUserState.CONNECTIVITY_ERROR
1550 : RtpEndUserState.CONNECTIVITY_LOST_ERROR;
1551 case TERMINATED_APPLICATION_FAILURE:
1552 return RtpEndUserState.APPLICATION_ERROR;
1553 case TERMINATED_SECURITY_ERROR:
1554 return RtpEndUserState.SECURITY_ERROR;
1555 }
1556 throw new IllegalStateException(
1557 String.format("%s has no equivalent EndUserState", this.state));
1558 }
1559
1560 private RtpEndUserState getPeerConnectionStateAsEndUserState() {
1561 final PeerConnection.PeerConnectionState state;
1562 try {
1563 state = webRTCWrapper.getState();
1564 } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
1565 // We usually close the WebRTCWrapper *before* transitioning so we might still
1566 // be in SESSION_ACCEPTED even though the peerConnection has been torn down
1567 return RtpEndUserState.ENDING_CALL;
1568 }
1569 switch (state) {
1570 case CONNECTED:
1571 return RtpEndUserState.CONNECTED;
1572 case NEW:
1573 case CONNECTING:
1574 return RtpEndUserState.CONNECTING;
1575 case CLOSED:
1576 return RtpEndUserState.ENDING_CALL;
1577 default:
1578 return zeroDuration()
1579 ? RtpEndUserState.CONNECTIVITY_ERROR
1580 : RtpEndUserState.RECONNECTING;
1581 }
1582 }
1583
1584 public Set<Media> getMedia() {
1585 final State current = getState();
1586 if (current == State.NULL) {
1587 if (isInitiator()) {
1588 return Preconditions.checkNotNull(
1589 this.proposedMedia, "RTP connection has not been initialized properly");
1590 }
1591 throw new IllegalStateException("RTP connection has not been initialized yet");
1592 }
1593 if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) {
1594 return Preconditions.checkNotNull(
1595 this.proposedMedia, "RTP connection has not been initialized properly");
1596 }
1597 final RtpContentMap initiatorContentMap = initiatorRtpContentMap;
1598 if (initiatorContentMap != null) {
1599 return initiatorContentMap.getMedia();
1600 } else if (isTerminated()) {
1601 return Collections.emptySet(); // we might fail before we ever got a chance to set media
1602 } else {
1603 return Preconditions.checkNotNull(
1604 this.proposedMedia, "RTP connection has not been initialized properly");
1605 }
1606 }
1607
1608 public boolean isVerified() {
1609 final String fingerprint = this.omemoVerification.getFingerprint();
1610 if (fingerprint == null) {
1611 return false;
1612 }
1613 final FingerprintStatus status =
1614 id.account.getAxolotlService().getFingerprintTrust(fingerprint);
1615 return status != null && status.isVerified();
1616 }
1617
1618 public synchronized void acceptCall() {
1619 switch (this.state) {
1620 case PROPOSED:
1621 cancelRingingTimeout();
1622 acceptCallFromProposed();
1623 break;
1624 case SESSION_INITIALIZED:
1625 cancelRingingTimeout();
1626 acceptCallFromSessionInitialized();
1627 break;
1628 case ACCEPTED:
1629 Log.w(
1630 Config.LOGTAG,
1631 id.account.getJid().asBareJid()
1632 + ": the call has already been accepted with another client. UI was just lagging behind");
1633 break;
1634 case PROCEED:
1635 case SESSION_ACCEPTED:
1636 Log.w(
1637 Config.LOGTAG,
1638 id.account.getJid().asBareJid()
1639 + ": the call has already been accepted. user probably double tapped the UI");
1640 break;
1641 default:
1642 throw new IllegalStateException("Can not accept call from " + this.state);
1643 }
1644 }
1645
1646 public void notifyPhoneCall() {
1647 Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections");
1648 if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) {
1649 rejectCall();
1650 } else {
1651 endCall();
1652 }
1653 }
1654
1655 public synchronized void rejectCall() {
1656 if (isTerminated()) {
1657 Log.w(
1658 Config.LOGTAG,
1659 id.account.getJid().asBareJid()
1660 + ": received rejectCall() when session has already been terminated. nothing to do");
1661 return;
1662 }
1663 switch (this.state) {
1664 case PROPOSED:
1665 rejectCallFromProposed();
1666 break;
1667 case SESSION_INITIALIZED:
1668 rejectCallFromSessionInitiate();
1669 break;
1670 default:
1671 throw new IllegalStateException("Can not reject call from " + this.state);
1672 }
1673 }
1674
1675 public synchronized void endCall() {
1676 if (isTerminated()) {
1677 Log.w(
1678 Config.LOGTAG,
1679 id.account.getJid().asBareJid()
1680 + ": received endCall() when session has already been terminated. nothing to do");
1681 return;
1682 }
1683 if (isInState(State.PROPOSED) && !isInitiator()) {
1684 rejectCallFromProposed();
1685 return;
1686 }
1687 if (isInState(State.PROCEED)) {
1688 if (isInitiator()) {
1689 retractFromProceed();
1690 } else {
1691 rejectCallFromProceed();
1692 }
1693 return;
1694 }
1695 if (isInitiator()
1696 && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) {
1697 this.webRTCWrapper.close();
1698 sendSessionTerminate(Reason.CANCEL);
1699 return;
1700 }
1701 if (isInState(State.SESSION_INITIALIZED)) {
1702 rejectCallFromSessionInitiate();
1703 return;
1704 }
1705 if (isInState(State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
1706 this.webRTCWrapper.close();
1707 sendSessionTerminate(Reason.SUCCESS);
1708 return;
1709 }
1710 if (isInState(
1711 State.TERMINATED_APPLICATION_FAILURE,
1712 State.TERMINATED_CONNECTIVITY_ERROR,
1713 State.TERMINATED_DECLINED_OR_BUSY)) {
1714 Log.d(
1715 Config.LOGTAG,
1716 "ignoring request to end call because already in state " + this.state);
1717 return;
1718 }
1719 throw new IllegalStateException(
1720 "called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator());
1721 }
1722
1723 private void retractFromProceed() {
1724 Log.d(Config.LOGTAG, "retract from proceed");
1725 this.sendJingleMessage("retract");
1726 closeTransitionLogFinish(State.RETRACTED_RACED);
1727 }
1728
1729 private void closeTransitionLogFinish(final State state) {
1730 this.webRTCWrapper.close();
1731 transitionOrThrow(state);
1732 writeLogMessage(state);
1733 finish();
1734 }
1735
1736 private void setupWebRTC(
1737 final Set<Media> media, final List<PeerConnection.IceServer> iceServers)
1738 throws WebRTCWrapper.InitializationException {
1739 this.jingleConnectionManager.ensureConnectionIsRegistered(this);
1740 final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference;
1741 if (media.contains(Media.VIDEO)) {
1742 speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.SPEAKER;
1743 } else {
1744 speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.EARPIECE;
1745 }
1746 this.webRTCWrapper.setup(this.xmppConnectionService, speakerPhonePreference);
1747 this.webRTCWrapper.initializePeerConnection(media, iceServers);
1748 }
1749
1750 private void acceptCallFromProposed() {
1751 transitionOrThrow(State.PROCEED);
1752 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1753 this.sendJingleMessage("accept", id.account.getJid().asBareJid());
1754 this.sendJingleMessage("proceed");
1755 }
1756
1757 private void rejectCallFromProposed() {
1758 transitionOrThrow(State.REJECTED);
1759 writeLogMessageMissed();
1760 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1761 this.sendJingleMessage("reject");
1762 finish();
1763 }
1764
1765 private void rejectCallFromProceed() {
1766 this.sendJingleMessage("reject");
1767 closeTransitionLogFinish(State.REJECTED_RACED);
1768 }
1769
1770 private void rejectCallFromSessionInitiate() {
1771 webRTCWrapper.close();
1772 sendSessionTerminate(Reason.DECLINE);
1773 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1774 }
1775
1776 private void sendJingleMessage(final String action) {
1777 sendJingleMessage(action, id.with);
1778 }
1779
1780 private void sendJingleMessage(final String action, final Jid to) {
1781 final MessagePacket messagePacket = new MessagePacket();
1782 messagePacket.setType(MessagePacket.TYPE_CHAT); // we want to carbon copy those
1783 messagePacket.setTo(to);
1784 final Element intent =
1785 messagePacket
1786 .addChild(action, Namespace.JINGLE_MESSAGE)
1787 .setAttribute("id", id.sessionId);
1788 if ("proceed".equals(action)) {
1789 messagePacket.setId(JINGLE_MESSAGE_PROCEED_ID_PREFIX + id.sessionId);
1790 if (isOmemoEnabled()) {
1791 final int deviceId = id.account.getAxolotlService().getOwnDeviceId();
1792 final Element device =
1793 intent.addChild("device", Namespace.OMEMO_DTLS_SRTP_VERIFICATION);
1794 device.setAttribute("id", deviceId);
1795 }
1796 }
1797 messagePacket.addChild("store", "urn:xmpp:hints");
1798 xmppConnectionService.sendMessagePacket(id.account, messagePacket);
1799 }
1800
1801 private boolean isOmemoEnabled() {
1802 final Conversational conversational = message.getConversation();
1803 if (conversational instanceof Conversation) {
1804 return ((Conversation) conversational).getNextEncryption()
1805 == Message.ENCRYPTION_AXOLOTL;
1806 }
1807 return false;
1808 }
1809
1810 private void acceptCallFromSessionInitialized() {
1811 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
1812 sendSessionAccept();
1813 }
1814
1815 private synchronized boolean isInState(State... state) {
1816 return Arrays.asList(state).contains(this.state);
1817 }
1818
1819 private boolean transition(final State target) {
1820 return transition(target, null);
1821 }
1822
1823 private synchronized boolean transition(final State target, final Runnable runnable) {
1824 final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
1825 if (validTransitions != null && validTransitions.contains(target)) {
1826 this.state = target;
1827 if (runnable != null) {
1828 runnable.run();
1829 }
1830 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
1831 updateEndUserState();
1832 updateOngoingCallNotification();
1833 return true;
1834 } else {
1835 return false;
1836 }
1837 }
1838
1839 void transitionOrThrow(final State target) {
1840 if (!transition(target)) {
1841 throw new IllegalStateException(
1842 String.format("Unable to transition from %s to %s", this.state, target));
1843 }
1844 }
1845
1846 @Override
1847 public void onIceCandidate(final IceCandidate iceCandidate) {
1848 final RtpContentMap rtpContentMap =
1849 isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
1850 final IceUdpTransportInfo.Credentials credentials;
1851 try {
1852 credentials = rtpContentMap.getCredentials(iceCandidate.sdpMid);
1853 } catch (final IllegalArgumentException e) {
1854 Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate, e);
1855 return;
1856 }
1857 final String uFrag = credentials.ufrag;
1858 final IceUdpTransportInfo.Candidate candidate =
1859 IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, uFrag);
1860 if (candidate == null) {
1861 Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate);
1862 return;
1863 }
1864 Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate);
1865 sendTransportInfo(iceCandidate.sdpMid, candidate);
1866 }
1867
1868 @Override
1869 public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
1870 Log.d(
1871 Config.LOGTAG,
1872 id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState);
1873 this.stateHistory.add(newState);
1874 if (newState == PeerConnection.PeerConnectionState.CONNECTED) {
1875 this.sessionDuration.start();
1876 updateOngoingCallNotification();
1877 } else if (this.sessionDuration.isRunning()) {
1878 this.sessionDuration.stop();
1879 updateOngoingCallNotification();
1880 }
1881
1882 final boolean neverConnected =
1883 !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED);
1884
1885 if (newState == PeerConnection.PeerConnectionState.FAILED) {
1886 if (neverConnected) {
1887 if (isTerminated()) {
1888 Log.d(
1889 Config.LOGTAG,
1890 id.account.getJid().asBareJid()
1891 + ": not sending session-terminate after connectivity error because session is already in state "
1892 + this.state);
1893 return;
1894 }
1895 webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection);
1896 return;
1897 } else {
1898 webRTCWrapper.restartIce();
1899 }
1900 }
1901 updateEndUserState();
1902 }
1903
1904 @Override
1905 public void onRenegotiationNeeded() {
1906 this.webRTCWrapper.execute(this::initiateIceRestart);
1907 }
1908
1909 private void initiateIceRestart() {
1910 // TODO discover new TURN/STUN credentials
1911 this.stateHistory.clear();
1912 this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
1913 final SessionDescription sessionDescription;
1914 try {
1915 sessionDescription = setLocalSessionDescription();
1916 } catch (final Exception e) {
1917 final Throwable cause = Throwables.getRootCause(e);
1918 Log.d(Config.LOGTAG, "failed to renegotiate", cause);
1919 sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
1920 return;
1921 }
1922 final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
1923 final RtpContentMap transportInfo = rtpContentMap.transportInfo();
1924 final JinglePacket jinglePacket =
1925 transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
1926 Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket);
1927 jinglePacket.setTo(id.with);
1928 xmppConnectionService.sendIqPacket(
1929 id.account,
1930 jinglePacket,
1931 (account, response) -> {
1932 if (response.getType() == IqPacket.TYPE.RESULT) {
1933 Log.d(Config.LOGTAG, "received success to our ice restart");
1934 setLocalContentMap(rtpContentMap);
1935 webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
1936 return;
1937 }
1938 if (response.getType() == IqPacket.TYPE.ERROR) {
1939 final Element error = response.findChild("error");
1940 if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) {
1941 Log.d(Config.LOGTAG, "received tie-break as result of ice restart");
1942 return;
1943 }
1944 handleIqErrorResponse(response);
1945 }
1946 if (response.getType() == IqPacket.TYPE.TIMEOUT) {
1947 handleIqTimeoutResponse(response);
1948 }
1949 });
1950 }
1951
1952 private void setLocalContentMap(final RtpContentMap rtpContentMap) {
1953 if (isInitiator()) {
1954 this.initiatorRtpContentMap = rtpContentMap;
1955 } else {
1956 this.responderRtpContentMap = rtpContentMap;
1957 }
1958 }
1959
1960 private void setRemoteContentMap(final RtpContentMap rtpContentMap) {
1961 if (isInitiator()) {
1962 this.responderRtpContentMap = rtpContentMap;
1963 } else {
1964 this.initiatorRtpContentMap = rtpContentMap;
1965 }
1966 }
1967
1968 private SessionDescription setLocalSessionDescription()
1969 throws ExecutionException, InterruptedException {
1970 final org.webrtc.SessionDescription sessionDescription =
1971 this.webRTCWrapper.setLocalDescription().get();
1972 return SessionDescription.parse(sessionDescription.description);
1973 }
1974
1975 private void closeWebRTCSessionAfterFailedConnection() {
1976 this.webRTCWrapper.close();
1977 synchronized (this) {
1978 if (isTerminated()) {
1979 Log.d(
1980 Config.LOGTAG,
1981 id.account.getJid().asBareJid()
1982 + ": no need to send session-terminate after failed connection. Other party already did");
1983 return;
1984 }
1985 sendSessionTerminate(Reason.CONNECTIVITY_ERROR);
1986 }
1987 }
1988
1989 public boolean zeroDuration() {
1990 return this.sessionDuration.elapsed(TimeUnit.NANOSECONDS) <= 0;
1991 }
1992
1993 public long getCallDuration() {
1994 return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS);
1995 }
1996
1997 public AppRTCAudioManager getAudioManager() {
1998 return webRTCWrapper.getAudioManager();
1999 }
2000
2001 public boolean isMicrophoneEnabled() {
2002 return webRTCWrapper.isMicrophoneEnabled();
2003 }
2004
2005 public boolean setMicrophoneEnabled(final boolean enabled) {
2006 return webRTCWrapper.setMicrophoneEnabled(enabled);
2007 }
2008
2009 public boolean isVideoEnabled() {
2010 return webRTCWrapper.isVideoEnabled();
2011 }
2012
2013 public void setVideoEnabled(final boolean enabled) {
2014 webRTCWrapper.setVideoEnabled(enabled);
2015 }
2016
2017 public boolean isCameraSwitchable() {
2018 return webRTCWrapper.isCameraSwitchable();
2019 }
2020
2021 public boolean isFrontCamera() {
2022 return webRTCWrapper.isFrontCamera();
2023 }
2024
2025 public ListenableFuture<Boolean> switchCamera() {
2026 return webRTCWrapper.switchCamera();
2027 }
2028
2029 @Override
2030 public void onAudioDeviceChanged(
2031 AppRTCAudioManager.AudioDevice selectedAudioDevice,
2032 Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
2033 xmppConnectionService.notifyJingleRtpConnectionUpdate(
2034 selectedAudioDevice, availableAudioDevices);
2035 }
2036
2037 private void updateEndUserState() {
2038 final RtpEndUserState endUserState = getEndUserState();
2039 jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia());
2040 xmppConnectionService.notifyJingleRtpConnectionUpdate(
2041 id.account, id.with, id.sessionId, endUserState);
2042 }
2043
2044 private void updateOngoingCallNotification() {
2045 final State state = this.state;
2046 if (STATES_SHOWING_ONGOING_CALL.contains(state)) {
2047 final boolean reconnecting;
2048 if (state == State.SESSION_ACCEPTED) {
2049 reconnecting =
2050 getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING;
2051 } else {
2052 reconnecting = false;
2053 }
2054 xmppConnectionService.setOngoingCall(id, getMedia(), reconnecting);
2055 } else {
2056 xmppConnectionService.removeOngoingCall();
2057 }
2058 }
2059
2060 private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) {
2061 if (id.account.getXmppConnection().getFeatures().externalServiceDiscovery()) {
2062 final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2063 request.setTo(id.account.getDomain());
2064 request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
2065 xmppConnectionService.sendIqPacket(
2066 id.account,
2067 request,
2068 (account, response) -> {
2069 ImmutableList.Builder<PeerConnection.IceServer> listBuilder =
2070 new ImmutableList.Builder<>();
2071 if (response.getType() == IqPacket.TYPE.RESULT) {
2072 final Element services =
2073 response.findChild(
2074 "services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
2075 final List<Element> children =
2076 services == null
2077 ? Collections.emptyList()
2078 : services.getChildren();
2079 for (final Element child : children) {
2080 if ("service".equals(child.getName())) {
2081 final String type = child.getAttribute("type");
2082 final String host = child.getAttribute("host");
2083 final String sport = child.getAttribute("port");
2084 final Integer port =
2085 sport == null ? null : Ints.tryParse(sport);
2086 final String transport = child.getAttribute("transport");
2087 final String username = child.getAttribute("username");
2088 final String password = child.getAttribute("password");
2089 if (Strings.isNullOrEmpty(host) || port == null) {
2090 continue;
2091 }
2092 if (port < 0 || port > 65535) {
2093 continue;
2094 }
2095 if (Arrays.asList("stun", "stuns", "turn", "turns")
2096 .contains(type)
2097 && Arrays.asList("udp", "tcp").contains(transport)) {
2098 if (Arrays.asList("stuns", "turns").contains(type)
2099 && "udp".equals(transport)) {
2100 Log.d(
2101 Config.LOGTAG,
2102 id.account.getJid().asBareJid()
2103 + ": skipping invalid combination of udp/tls in external services");
2104 continue;
2105 }
2106 final PeerConnection.IceServer.Builder iceServerBuilder =
2107 PeerConnection.IceServer.builder(
2108 String.format(
2109 "%s:%s:%s?transport=%s",
2110 type,
2111 IP.wrapIPv6(host),
2112 port,
2113 transport));
2114 iceServerBuilder.setTlsCertPolicy(
2115 PeerConnection.TlsCertPolicy
2116 .TLS_CERT_POLICY_INSECURE_NO_CHECK);
2117 if (username != null && password != null) {
2118 iceServerBuilder.setUsername(username);
2119 iceServerBuilder.setPassword(password);
2120 } else if (Arrays.asList("turn", "turns").contains(type)) {
2121 // The WebRTC spec requires throwing an
2122 // InvalidAccessError when username (from libwebrtc
2123 // source coder)
2124 // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
2125 Log.d(
2126 Config.LOGTAG,
2127 id.account.getJid().asBareJid()
2128 + ": skipping "
2129 + type
2130 + "/"
2131 + transport
2132 + " without username and password");
2133 continue;
2134 }
2135 final PeerConnection.IceServer iceServer =
2136 iceServerBuilder.createIceServer();
2137 Log.d(
2138 Config.LOGTAG,
2139 id.account.getJid().asBareJid()
2140 + ": discovered ICE Server: "
2141 + iceServer);
2142 listBuilder.add(iceServer);
2143 }
2144 }
2145 }
2146 }
2147 final List<PeerConnection.IceServer> iceServers = listBuilder.build();
2148 if (iceServers.size() == 0) {
2149 Log.w(
2150 Config.LOGTAG,
2151 id.account.getJid().asBareJid()
2152 + ": no ICE server found "
2153 + response);
2154 }
2155 onIceServersDiscovered.onIceServersDiscovered(iceServers);
2156 });
2157 } else {
2158 Log.w(
2159 Config.LOGTAG,
2160 id.account.getJid().asBareJid() + ": has no external service discovery");
2161 onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList());
2162 }
2163 }
2164
2165 private void finish() {
2166 if (isTerminated()) {
2167 this.cancelRingingTimeout();
2168 this.webRTCWrapper.verifyClosed();
2169 this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
2170 this.jingleConnectionManager.finishConnectionOrThrow(this);
2171 } else {
2172 throw new IllegalStateException(
2173 String.format("Unable to call finish from %s", this.state));
2174 }
2175 }
2176
2177 private void writeLogMessage(final State state) {
2178 final long duration = getCallDuration();
2179 if (state == State.TERMINATED_SUCCESS
2180 || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) {
2181 writeLogMessageSuccess(duration);
2182 } else {
2183 writeLogMessageMissed();
2184 }
2185 }
2186
2187 private void writeLogMessageSuccess(final long duration) {
2188 this.message.setBody(new RtpSessionStatus(true, duration).toString());
2189 this.writeMessage();
2190 }
2191
2192 private void writeLogMessageMissed() {
2193 this.message.setBody(new RtpSessionStatus(false, 0).toString());
2194 this.writeMessage();
2195 }
2196
2197 private void writeMessage() {
2198 final Conversational conversational = message.getConversation();
2199 if (conversational instanceof Conversation) {
2200 ((Conversation) conversational).add(this.message);
2201 xmppConnectionService.createMessageAsync(message);
2202 xmppConnectionService.updateConversationUi();
2203 } else {
2204 throw new IllegalStateException("Somehow the conversation in a message was a stub");
2205 }
2206 }
2207
2208 public State getState() {
2209 return this.state;
2210 }
2211
2212 boolean isTerminated() {
2213 return TERMINATED.contains(this.state);
2214 }
2215
2216 public Optional<VideoTrack> getLocalVideoTrack() {
2217 return webRTCWrapper.getLocalVideoTrack();
2218 }
2219
2220 public Optional<VideoTrack> getRemoteVideoTrack() {
2221 return webRTCWrapper.getRemoteVideoTrack();
2222 }
2223
2224 public EglBase.Context getEglBaseContext() {
2225 return webRTCWrapper.getEglBaseContext();
2226 }
2227
2228 void setProposedMedia(final Set<Media> media) {
2229 this.proposedMedia = media;
2230 }
2231
2232 public void fireStateUpdate() {
2233 final RtpEndUserState endUserState = getEndUserState();
2234 xmppConnectionService.notifyJingleRtpConnectionUpdate(
2235 id.account, id.with, id.sessionId, endUserState);
2236 }
2237
2238 private interface OnIceServersDiscovered {
2239 void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
2240 }
2241}