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