1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Log;
4
5import com.google.common.collect.ImmutableList;
6import com.google.common.collect.ImmutableMap;
7
8import org.webrtc.IceCandidate;
9import org.webrtc.PeerConnection;
10
11import java.util.ArrayDeque;
12import java.util.Arrays;
13import java.util.Collection;
14import java.util.Collections;
15import java.util.List;
16import java.util.Map;
17
18import eu.siacs.conversations.Config;
19import eu.siacs.conversations.xml.Element;
20import eu.siacs.conversations.xml.Namespace;
21import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
22import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
23import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
24import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
25import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
26import rocks.xmpp.addr.Jid;
27
28public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback {
29
30 private static final Map<State, Collection<State>> VALID_TRANSITIONS;
31
32 static {
33 final ImmutableMap.Builder<State, Collection<State>> transitionBuilder = new ImmutableMap.Builder<>();
34 transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED));
35 transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED, State.RETRACTED));
36 transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED_PRE_APPROVED));
37 transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT));
38 transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT));
39 transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of(State.TERMINATED_SUCCESS, State.TERMINATED_CONNECTIVITY_ERROR));
40 VALID_TRANSITIONS = transitionBuilder.build();
41 }
42
43 private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
44 private final ArrayDeque<IceCandidate> pendingIceCandidates = new ArrayDeque<>();
45 private State state = State.NULL;
46 private RtpContentMap initiatorRtpContentMap;
47 private RtpContentMap responderRtpContentMap;
48
49
50 public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
51 super(jingleConnectionManager, id, initiator);
52 }
53
54 @Override
55 void deliverPacket(final JinglePacket jinglePacket) {
56 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection");
57 switch (jinglePacket.getAction()) {
58 case SESSION_INITIATE:
59 receiveSessionInitiate(jinglePacket);
60 break;
61 case TRANSPORT_INFO:
62 receiveTransportInfo(jinglePacket);
63 break;
64 case SESSION_ACCEPT:
65 receiveSessionAccept(jinglePacket);
66 break;
67 case SESSION_TERMINATE:
68 receiveSessionTerminate(jinglePacket);
69 break;
70 default:
71 Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction()));
72 break;
73 }
74 }
75
76 private void receiveSessionTerminate(final JinglePacket jinglePacket) {
77 final Reason reason = jinglePacket.getReason();
78 final State previous = this.state;
79 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + reason + " while in state " + previous);
80 webRTCWrapper.close();
81 transitionOrThrow(reasonToState(reason));
82 if (previous == State.PROPOSED || previous == State.SESSION_INITIALIZED) {
83 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
84 }
85 jingleConnectionManager.finishConnection(this);
86 }
87
88 private static State reasonToState(Reason reason) {
89 switch (reason) {
90 case SUCCESS:
91 return State.TERMINATED_SUCCESS;
92 case DECLINE:
93 case BUSY:
94 return State.TERMINATED_DECLINED_OR_BUSY;
95 case CANCEL:
96 case TIMEOUT:
97 return State.TERMINATED_CANCEL_OR_TIMEOUT;
98 default:
99 return State.TERMINATED_CONNECTIVITY_ERROR;
100 }
101 }
102
103 private void receiveTransportInfo(final JinglePacket jinglePacket) {
104 if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
105 final RtpContentMap contentMap;
106 try {
107 contentMap = RtpContentMap.of(jinglePacket);
108 } catch (IllegalArgumentException | NullPointerException e) {
109 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e);
110 return;
111 }
112 final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
113 final Group originalGroup = rtpContentMap != null ? rtpContentMap.group : null;
114 final List<String> identificationTags = originalGroup == null ? Collections.emptyList() : originalGroup.getIdentificationTags();
115 if (identificationTags.size() == 0) {
116 Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices");
117 }
118 for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contentMap.contents.entrySet()) {
119 final String ufrag = content.getValue().transport.getAttribute("ufrag");
120 for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) {
121 final String sdp = candidate.toSdpAttribute(ufrag);
122 final String sdpMid = content.getKey();
123 final int mLineIndex = identificationTags.indexOf(sdpMid);
124 final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp);
125 Log.d(Config.LOGTAG, "received candidate: " + iceCandidate);
126 if (isInState(State.SESSION_ACCEPTED)) {
127 this.webRTCWrapper.addIceCandidate(iceCandidate);
128 } else {
129 this.pendingIceCandidates.push(iceCandidate);
130 }
131 }
132 }
133 } else {
134 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state);
135 }
136 }
137
138 private void receiveSessionInitiate(final JinglePacket jinglePacket) {
139 if (isInitiator()) {
140 Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid()));
141 //TODO respond with out-of-order
142 return;
143 }
144 final RtpContentMap contentMap;
145 try {
146 contentMap = RtpContentMap.of(jinglePacket);
147 contentMap.requireContentDescriptions();
148 } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) {
149 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e);
150 return;
151 }
152 Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents");
153 final State target;
154 if (this.state == State.PROCEED) {
155 target = State.SESSION_INITIALIZED_PRE_APPROVED;
156 } else {
157 target = State.SESSION_INITIALIZED;
158 }
159 if (transition(target)) {
160 this.initiatorRtpContentMap = contentMap;
161 if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
162 Log.d(Config.LOGTAG, "automatically accepting");
163 sendSessionAccept();
164 } else {
165 Log.d(Config.LOGTAG, "start ringing");
166 //TODO start ringing
167 }
168 } else {
169 Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state));
170 }
171 }
172
173 private void receiveSessionAccept(final JinglePacket jinglePacket) {
174 if (!isInitiator()) {
175 Log.d(Config.LOGTAG, String.format("%s: received session-accept even though we were responding", id.account.getJid().asBareJid()));
176 //TODO respond with out-of-order
177 return;
178 }
179 final RtpContentMap contentMap;
180 try {
181 contentMap = RtpContentMap.of(jinglePacket);
182 contentMap.requireContentDescriptions();
183 } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) {
184 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e);
185 return;
186 }
187 Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents");
188 if (transition(State.SESSION_ACCEPTED)) {
189 receiveSessionAccept(contentMap);
190 } else {
191 Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state));
192 //TODO out-of-order
193 }
194 }
195
196 private void receiveSessionAccept(final RtpContentMap contentMap) {
197 this.responderRtpContentMap = contentMap;
198 org.webrtc.SessionDescription answer = new org.webrtc.SessionDescription(
199 org.webrtc.SessionDescription.Type.ANSWER,
200 SessionDescription.of(contentMap).toString()
201 );
202 try {
203 this.webRTCWrapper.setRemoteDescription(answer).get();
204 } catch (Exception e) {
205 Log.d(Config.LOGTAG, "unable to receive session accept", e);
206 }
207 }
208
209 private void sendSessionAccept() {
210 final RtpContentMap rtpContentMap = this.initiatorRtpContentMap;
211 if (rtpContentMap == null) {
212 throw new IllegalStateException("initiator RTP Content Map has not been set");
213 }
214 setupWebRTC();
215 final org.webrtc.SessionDescription offer = new org.webrtc.SessionDescription(
216 org.webrtc.SessionDescription.Type.OFFER,
217 SessionDescription.of(rtpContentMap).toString()
218 );
219 try {
220 this.webRTCWrapper.setRemoteDescription(offer).get();
221 org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get();
222 final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
223 final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
224 sendSessionAccept(respondingRtpContentMap);
225 this.webRTCWrapper.setLocalDescription(webRTCSessionDescription);
226 } catch (Exception e) {
227 Log.d(Config.LOGTAG, "unable to send session accept", e);
228
229 }
230 }
231
232 private void sendSessionAccept(final RtpContentMap rtpContentMap) {
233 this.responderRtpContentMap = rtpContentMap;
234 this.transitionOrThrow(State.SESSION_ACCEPTED);
235 final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
236 Log.d(Config.LOGTAG, sessionAccept.toString());
237 send(sessionAccept);
238 }
239
240 void deliveryMessage(final Jid from, final Element message) {
241 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message);
242 switch (message.getName()) {
243 case "propose":
244 receivePropose(from, message);
245 break;
246 case "proceed":
247 receiveProceed(from, message);
248 break;
249 case "retract":
250 receiveRetract(from, message);
251 break;
252 case "reject":
253 receiveReject(from, message);
254 break;
255 case "accept":
256 receiveAccept(from, message);
257 break;
258 default:
259 break;
260 }
261 }
262
263 private void receiveAccept(Jid from, Element message) {
264 final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid());
265 if (originatedFromMyself) {
266 if (transition(State.ACCEPTED)) {
267 this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
268 this.jingleConnectionManager.finishConnection(this);
269 } else {
270 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to transition to accept because already in state=" + this.state);
271 }
272 } else {
273 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring 'accept' from " + from);
274 }
275 }
276
277 private void receiveReject(Jid from, Element message) {
278 final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid());
279 //reject from another one of my clients
280 if (originatedFromMyself) {
281 if (transition(State.REJECTED)) {
282 this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
283 this.jingleConnectionManager.finishConnection(this);
284 } else {
285 Log.d(Config.LOGTAG, "not able to transition into REJECTED because already in " + this.state);
286 }
287 } else {
288 Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from " + from + " for session with " + id.with);
289 }
290 }
291
292 private void receivePropose(final Jid from, final Element propose) {
293 final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid());
294 //TODO we can use initiator logic here
295 if (originatedFromMyself) {
296 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring");
297 } else if (transition(State.PROPOSED)) {
298 startRinging();
299 } else {
300 Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state);
301 }
302 }
303
304 private void startRinging() {
305 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received call from " + id.with + ". start ringing");
306 xmppConnectionService.getNotificationService().showIncomingCallNotification(id);
307 }
308
309 private void receiveProceed(final Jid from, final Element proceed) {
310 if (from.equals(id.with)) {
311 if (isInitiator()) {
312 if (transition(State.PROCEED)) {
313 this.sendSessionInitiate(State.SESSION_INITIALIZED_PRE_APPROVED);
314 } else {
315 Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state));
316 }
317 } else {
318 Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because we were not initializing", id.account.getJid().asBareJid()));
319 }
320 } else if (from.asBareJid().equals(id.account.getJid().asBareJid())) {
321 if (transition(State.ACCEPTED)) {
322 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": moved session with " + id.with + " into state accepted after received carbon copied procced");
323 this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
324 this.jingleConnectionManager.finishConnection(this);
325 }
326 } else {
327 //TODO a carbon copied proceed from another client of mine has the same logic as `accept`
328 Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with));
329 }
330 }
331
332 private void receiveRetract(final Jid from, final Element retract) {
333 if (from.equals(id.with)) {
334 if (transition(State.RETRACTED)) {
335 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
336 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted");
337 //TODO create missed call notification/message
338 jingleConnectionManager.finishConnection(this);
339 } else {
340 Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state);
341 }
342 } else {
343 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received retract from " + from + ". expected retract from" + id.with + ". ignoring");
344 }
345 }
346
347 private void sendSessionInitiate(final State targetState) {
348 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
349 setupWebRTC();
350 try {
351 org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get();
352 final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
353 Log.d(Config.LOGTAG, "description: " + webRTCSessionDescription.description);
354 final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
355 sendSessionInitiate(rtpContentMap, targetState);
356 this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
357 } catch (Exception e) {
358 Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e);
359 }
360 }
361
362 private void sendSessionInitiate(RtpContentMap rtpContentMap, final State targetState) {
363 this.initiatorRtpContentMap = rtpContentMap;
364 this.transitionOrThrow(targetState);
365 final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
366 Log.d(Config.LOGTAG, sessionInitiate.toString());
367 send(sessionInitiate);
368 }
369
370 private void sendSessionTerminate(final Reason reason) {
371 final State target = reasonToState(reason);
372 transitionOrThrow(target);
373 final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
374 jinglePacket.setReason(reason);
375 send(jinglePacket);
376 Log.d(Config.LOGTAG, jinglePacket.toString());
377 jingleConnectionManager.finishConnection(this);
378 }
379
380 private void sendTransportInfo(final String contentName, IceUdpTransportInfo.Candidate candidate) {
381 final RtpContentMap transportInfo;
382 try {
383 final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
384 transportInfo = rtpContentMap.transportInfo(contentName, candidate);
385 } catch (Exception e) {
386 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName);
387 return;
388 }
389 final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
390 Log.d(Config.LOGTAG, jinglePacket.toString());
391 send(jinglePacket);
392 }
393
394 private void send(final JinglePacket jinglePacket) {
395 jinglePacket.setTo(id.with);
396 //TODO track errors
397 xmppConnectionService.sendIqPacket(id.account, jinglePacket, null);
398 }
399
400 public RtpEndUserState getEndUserState() {
401 switch (this.state) {
402 case PROPOSED:
403 case SESSION_INITIALIZED:
404 if (isInitiator()) {
405 return RtpEndUserState.RINGING;
406 } else {
407 return RtpEndUserState.INCOMING_CALL;
408 }
409 case PROCEED:
410 if (isInitiator()) {
411 return RtpEndUserState.CONNECTING;
412 } else {
413 return RtpEndUserState.ACCEPTING_CALL;
414 }
415 case SESSION_INITIALIZED_PRE_APPROVED:
416 return RtpEndUserState.CONNECTING;
417 case SESSION_ACCEPTED:
418 final PeerConnection.PeerConnectionState state = webRTCWrapper.getState();
419 if (state == PeerConnection.PeerConnectionState.CONNECTED) {
420 return RtpEndUserState.CONNECTED;
421 } else if (state == PeerConnection.PeerConnectionState.NEW || state == PeerConnection.PeerConnectionState.CONNECTING) {
422 return RtpEndUserState.CONNECTING;
423 } else if (state == PeerConnection.PeerConnectionState.CLOSED) {
424 return RtpEndUserState.ENDING_CALL;
425 } else if (state == PeerConnection.PeerConnectionState.FAILED) {
426 return RtpEndUserState.CONNECTIVITY_ERROR;
427 } else {
428 return RtpEndUserState.ENDING_CALL;
429 }
430 case REJECTED:
431 case TERMINATED_DECLINED_OR_BUSY:
432 if (isInitiator()) {
433 return RtpEndUserState.DECLINED_OR_BUSY;
434 } else {
435 return RtpEndUserState.ENDED;
436 }
437 case TERMINATED_SUCCESS:
438 case ACCEPTED:
439 case RETRACTED:
440 case TERMINATED_CANCEL_OR_TIMEOUT:
441 return RtpEndUserState.ENDED;
442 case TERMINATED_CONNECTIVITY_ERROR:
443 return RtpEndUserState.CONNECTIVITY_ERROR;
444 }
445 throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state));
446 }
447
448
449 public void acceptCall() {
450 switch (this.state) {
451 case PROPOSED:
452 acceptCallFromProposed();
453 break;
454 case SESSION_INITIALIZED:
455 acceptCallFromSessionInitialized();
456 break;
457 default:
458 throw new IllegalStateException("Can not accept call from " + this.state);
459 }
460 }
461
462 public void rejectCall() {
463 switch (this.state) {
464 case PROPOSED:
465 rejectCallFromProposed();
466 break;
467 default:
468 throw new IllegalStateException("Can not reject call from " + this.state);
469 }
470 }
471
472 public void endCall() {
473 if (isInitiator() && isInState(State.SESSION_INITIALIZED)) {
474 webRTCWrapper.close();
475 sendSessionTerminate(Reason.CANCEL);
476 } else if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
477 webRTCWrapper.close();
478 sendSessionTerminate(Reason.SUCCESS);
479 } else {
480 throw new IllegalStateException("called 'endCall' while in state " + this.state);
481 }
482 }
483
484 private void setupWebRTC() {
485 this.webRTCWrapper.setup(this.xmppConnectionService);
486 this.webRTCWrapper.initializePeerConnection();
487 }
488
489 private void acceptCallFromProposed() {
490 transitionOrThrow(State.PROCEED);
491 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
492 this.sendJingleMessage("accept", id.account.getJid().asBareJid());
493 this.sendJingleMessage("proceed");
494 }
495
496 private void rejectCallFromProposed() {
497 transitionOrThrow(State.REJECTED);
498 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
499 this.sendJingleMessage("reject");
500 jingleConnectionManager.finishConnection(this);
501 }
502
503 private void sendJingleMessage(final String action) {
504 sendJingleMessage(action, id.with);
505 }
506
507 private void sendJingleMessage(final String action, final Jid to) {
508 final MessagePacket messagePacket = new MessagePacket();
509 messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
510 messagePacket.setTo(to);
511 messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId);
512 Log.d(Config.LOGTAG, messagePacket.toString());
513 xmppConnectionService.sendMessagePacket(id.account, messagePacket);
514 }
515
516 private void acceptCallFromSessionInitialized() {
517 xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
518 throw new IllegalStateException("accepting from this state has not been implemented yet");
519 }
520
521 private synchronized boolean isInState(State... state) {
522 return Arrays.asList(state).contains(this.state);
523 }
524
525 private synchronized boolean transition(final State target) {
526 final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
527 if (validTransitions != null && validTransitions.contains(target)) {
528 this.state = target;
529 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
530 updateEndUserState();
531 return true;
532 } else {
533 return false;
534 }
535 }
536
537 public void transitionOrThrow(final State target) {
538 if (!transition(target)) {
539 throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target));
540 }
541 }
542
543 @Override
544 public void onIceCandidate(final IceCandidate iceCandidate) {
545 final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp);
546 Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString());
547 sendTransportInfo(iceCandidate.sdpMid, candidate);
548 }
549
550 @Override
551 public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
552 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState);
553 updateEndUserState();
554 if (newState == PeerConnection.PeerConnectionState.FAILED) { //TODO guard this in isState(initiated,initated_approved,accepted) otherwise it might fire too late
555 sendSessionTerminate(Reason.CONNECTIVITY_ERROR);
556 }
557 }
558
559 private void updateEndUserState() {
560 xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, getEndUserState());
561 }
562}