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