1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Base64;
4import android.util.Log;
5
6import com.google.common.base.Objects;
7import com.google.common.base.Optional;
8import com.google.common.base.Preconditions;
9import com.google.common.cache.Cache;
10import com.google.common.cache.CacheBuilder;
11import com.google.common.collect.Collections2;
12import com.google.common.collect.ComparisonChain;
13import com.google.common.collect.ImmutableSet;
14
15import java.lang.ref.WeakReference;
16import java.security.SecureRandom;
17import java.util.Collection;
18import java.util.Collections;
19import java.util.HashMap;
20import java.util.List;
21import java.util.Map;
22import java.util.Set;
23import java.util.concurrent.ConcurrentHashMap;
24import java.util.concurrent.Executors;
25import java.util.concurrent.ScheduledExecutorService;
26import java.util.concurrent.ScheduledFuture;
27import java.util.concurrent.TimeUnit;
28
29import eu.siacs.conversations.Config;
30import eu.siacs.conversations.entities.Account;
31import eu.siacs.conversations.entities.Contact;
32import eu.siacs.conversations.entities.Conversation;
33import eu.siacs.conversations.entities.Conversational;
34import eu.siacs.conversations.entities.Message;
35import eu.siacs.conversations.entities.RtpSessionStatus;
36import eu.siacs.conversations.entities.Transferable;
37import eu.siacs.conversations.services.AbstractConnectionManager;
38import eu.siacs.conversations.services.XmppConnectionService;
39import eu.siacs.conversations.xml.Element;
40import eu.siacs.conversations.xml.Namespace;
41import eu.siacs.conversations.xmpp.Jid;
42import eu.siacs.conversations.xmpp.OnIqPacketReceived;
43import eu.siacs.conversations.xmpp.XmppConnection;
44import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
45import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
46import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
47import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
48import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
49import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
50import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
51import eu.siacs.conversations.xmpp.stanzas.IqPacket;
52import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
53
54public class JingleConnectionManager extends AbstractConnectionManager {
55 static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
56 Executors.newSingleThreadScheduledExecutor();
57 final ToneManager toneManager;
58 private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals =
59 new HashMap<>();
60 private final ConcurrentHashMap<AbstractJingleConnection.Id, AbstractJingleConnection>
61 connections = new ConcurrentHashMap<>();
62
63 private final Cache<PersistableSessionId, TerminatedRtpSession> terminatedSessions =
64 CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build();
65
66 private final HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
67
68 public JingleConnectionManager(XmppConnectionService service) {
69 super(service);
70 this.toneManager = new ToneManager(service);
71 }
72
73 static String nextRandomId() {
74 final byte[] id = new byte[16];
75 new SecureRandom().nextBytes(id);
76 return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING);
77 }
78
79 public void deliverPacket(final Account account, final JinglePacket packet) {
80 final String sessionId = packet.getSessionId();
81 if (sessionId == null) {
82 respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
83 return;
84 }
85 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet);
86 final AbstractJingleConnection existingJingleConnection = connections.get(id);
87 if (existingJingleConnection != null) {
88 existingJingleConnection.deliverPacket(packet);
89 } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) {
90 final Jid from = packet.getFrom();
91 final Content content = packet.getJingleContent();
92 final String descriptionNamespace =
93 content == null ? null : content.getDescriptionNamespace();
94 final AbstractJingleConnection connection;
95 if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
96 connection = new JingleFileTransferConnection(this, id, from);
97 } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)
98 && isUsingClearNet(account)) {
99 final boolean sessionEnded =
100 this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id));
101 final boolean stranger =
102 isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
103 if (isBusy() || sessionEnded || stranger) {
104 Log.d(
105 Config.LOGTAG,
106 id.account.getJid().asBareJid()
107 + ": rejected session with "
108 + id.with
109 + " because busy. sessionEnded="
110 + sessionEnded
111 + ", stranger="
112 + stranger);
113 mXmppConnectionService.sendIqPacket(
114 account, packet.generateResponse(IqPacket.TYPE.RESULT), null);
115 final JinglePacket sessionTermination =
116 new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
117 sessionTermination.setTo(id.with);
118 sessionTermination.setReason(Reason.BUSY, null);
119 mXmppConnectionService.sendIqPacket(account, sessionTermination, null);
120 return;
121 }
122 connection = new JingleRtpConnection(this, id, from);
123 } else {
124 respondWithJingleError(
125 account, packet, "unsupported-info", "feature-not-implemented", "cancel");
126 return;
127 }
128 connections.put(id, connection);
129 mXmppConnectionService.updateConversationUi();
130 connection.deliverPacket(packet);
131 } else {
132 Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet);
133 respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
134 }
135 }
136
137 private boolean isUsingClearNet(final Account account) {
138 return !account.isOnion() && !mXmppConnectionService.useTorToConnect();
139 }
140
141 public boolean isBusy() {
142 if (mXmppConnectionService.isPhoneInCall()) {
143 return true;
144 }
145 for (AbstractJingleConnection connection : this.connections.values()) {
146 if (connection instanceof JingleRtpConnection) {
147 if (((JingleRtpConnection) connection).isTerminated()) {
148 continue;
149 }
150 return true;
151 }
152 }
153 synchronized (this.rtpSessionProposals) {
154 return this.rtpSessionProposals.containsValue(DeviceDiscoveryState.DISCOVERED)
155 || this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING)
156 || this.rtpSessionProposals.containsValue(
157 DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED);
158 }
159 }
160
161 public void notifyPhoneCallStarted() {
162 for (AbstractJingleConnection connection : connections.values()) {
163 if (connection instanceof JingleRtpConnection) {
164 final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
165 if (rtpConnection.isTerminated()) {
166 continue;
167 }
168 rtpConnection.notifyPhoneCall();
169 }
170 }
171 }
172
173 private Optional<RtpSessionProposal> findMatchingSessionProposal(
174 final Account account, final Jid with, final Set<Media> media) {
175 synchronized (this.rtpSessionProposals) {
176 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
177 this.rtpSessionProposals.entrySet()) {
178 final RtpSessionProposal proposal = entry.getKey();
179 final DeviceDiscoveryState state = entry.getValue();
180 final boolean openProposal =
181 state == DeviceDiscoveryState.DISCOVERED
182 || state == DeviceDiscoveryState.SEARCHING
183 || state == DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED;
184 if (openProposal
185 && proposal.account == account
186 && proposal.with.equals(with.asBareJid())
187 && proposal.media.equals(media)) {
188 return Optional.of(proposal);
189 }
190 }
191 }
192 return Optional.absent();
193 }
194
195 private boolean hasMatchingRtpSession(
196 final Account account, final Jid with, final Set<Media> media) {
197 for (AbstractJingleConnection connection : this.connections.values()) {
198 if (connection instanceof JingleRtpConnection) {
199 final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
200 if (rtpConnection.isTerminated()) {
201 continue;
202 }
203 if (rtpConnection.getId().account == account
204 && rtpConnection.getId().with.asBareJid().equals(with.asBareJid())
205 && rtpConnection.getMedia().equals(media)) {
206 return true;
207 }
208 }
209 }
210 return false;
211 }
212
213 private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) {
214 final boolean notifyForStrangers =
215 mXmppConnectionService.getNotificationService().notificationsFromStrangers();
216 if (notifyForStrangers) {
217 return false;
218 }
219 final Contact contact = account.getRoster().getContact(with);
220 return !contact.showInContactList();
221 }
222
223 ScheduledFuture<?> schedule(
224 final Runnable runnable, final long delay, final TimeUnit timeUnit) {
225 return SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, timeUnit);
226 }
227
228 void respondWithJingleError(
229 final Account account,
230 final IqPacket original,
231 String jingleCondition,
232 String condition,
233 String conditionType) {
234 final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR);
235 final Element error = response.addChild("error");
236 error.setAttribute("type", conditionType);
237 error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
238 error.addChild(jingleCondition, Namespace.JINGLE_ERRORS);
239 account.getXmppConnection().sendIqPacket(response, null);
240 }
241
242 public void deliverMessage(
243 final Account account,
244 final Jid to,
245 final Jid from,
246 final Element message,
247 String remoteMsgId,
248 String serverMsgId,
249 long timestamp) {
250 Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace()));
251 final String sessionId = message.getAttribute("id");
252 if (sessionId == null) {
253 return;
254 }
255 if ("accept".equals(message.getName())) {
256 for (AbstractJingleConnection connection : connections.values()) {
257 if (connection instanceof JingleRtpConnection) {
258 final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
259 final AbstractJingleConnection.Id id = connection.getId();
260 if (id.account == account && id.sessionId.equals(sessionId)) {
261 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
262 return;
263 }
264 }
265 }
266 return;
267 }
268 final boolean fromSelf = from.asBareJid().equals(account.getJid().asBareJid());
269 // XEP version 0.6.0 sends proceed, reject, ringing to bare jid
270 final boolean addressedDirectly = to != null && to.equals(account.getJid());
271 final AbstractJingleConnection.Id id;
272 if (fromSelf) {
273 if (to != null && to.isFullJid()) {
274 id = AbstractJingleConnection.Id.of(account, to, sessionId);
275 } else {
276 return;
277 }
278 } else {
279 id = AbstractJingleConnection.Id.of(account, from, sessionId);
280 }
281 final AbstractJingleConnection existingJingleConnection = connections.get(id);
282 if (existingJingleConnection != null) {
283 if (existingJingleConnection instanceof JingleRtpConnection) {
284 ((JingleRtpConnection) existingJingleConnection)
285 .deliveryMessage(from, message, serverMsgId, timestamp);
286 } else {
287 Log.d(
288 Config.LOGTAG,
289 account.getJid().asBareJid()
290 + ": "
291 + existingJingleConnection.getClass().getName()
292 + " does not support jingle messages");
293 }
294 return;
295 }
296
297 if (fromSelf) {
298 if ("proceed".equals(message.getName())) {
299 final Conversation c =
300 mXmppConnectionService.findOrCreateConversation(
301 account, id.with, false, false);
302 final Message previousBusy = c.findRtpSession(sessionId, Message.STATUS_RECEIVED);
303 if (previousBusy != null) {
304 previousBusy.setBody(new RtpSessionStatus(true, 0).toString());
305 if (serverMsgId != null) {
306 previousBusy.setServerMsgId(serverMsgId);
307 }
308 previousBusy.setTime(timestamp);
309 mXmppConnectionService.updateMessage(previousBusy, true);
310 Log.d(
311 Config.LOGTAG,
312 id.account.getJid().asBareJid()
313 + ": updated previous busy because call got picked up by another device");
314 return;
315 }
316 }
317 // TODO handle reject for cases where we don’t have carbon copies (normally reject is to
318 // be sent to own bare jid as well)
319 Log.d(
320 Config.LOGTAG,
321 account.getJid().asBareJid() + ": ignore jingle message from self");
322 return;
323 }
324
325 if ("propose".equals(message.getName())) {
326 final Propose propose = Propose.upgrade(message);
327 final List<GenericDescription> descriptions = propose.getDescriptions();
328 final Collection<RtpDescription> rtpDescriptions =
329 Collections2.transform(
330 Collections2.filter(descriptions, d -> d instanceof RtpDescription),
331 input -> (RtpDescription) input);
332 if (rtpDescriptions.size() > 0
333 && rtpDescriptions.size() == descriptions.size()
334 && isUsingClearNet(account)) {
335 final Collection<Media> media =
336 Collections2.transform(rtpDescriptions, RtpDescription::getMedia);
337 if (media.contains(Media.UNKNOWN)) {
338 Log.d(
339 Config.LOGTAG,
340 account.getJid().asBareJid()
341 + ": encountered unknown media in session proposal. "
342 + propose);
343 return;
344 }
345 final Optional<RtpSessionProposal> matchingSessionProposal =
346 findMatchingSessionProposal(account, id.with, ImmutableSet.copyOf(media));
347 if (matchingSessionProposal.isPresent()) {
348 final String ourSessionId = matchingSessionProposal.get().sessionId;
349 final String theirSessionId = id.sessionId;
350 if (ComparisonChain.start()
351 .compare(ourSessionId, theirSessionId)
352 .compare(
353 account.getJid().toEscapedString(),
354 id.with.toEscapedString())
355 .result()
356 > 0) {
357 Log.d(
358 Config.LOGTAG,
359 account.getJid().asBareJid()
360 + ": our session lost tie break. automatically accepting their session. winning Session="
361 + theirSessionId);
362 // TODO a retract for this reason should probably include some indication of
363 // tie break
364 retractSessionProposal(matchingSessionProposal.get());
365 final JingleRtpConnection rtpConnection =
366 new JingleRtpConnection(this, id, from);
367 this.connections.put(id, rtpConnection);
368 rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
369 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
370 } else {
371 Log.d(
372 Config.LOGTAG,
373 account.getJid().asBareJid()
374 + ": our session won tie break. waiting for other party to accept. winningSession="
375 + ourSessionId);
376 }
377 return;
378 }
379 final boolean stranger =
380 isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
381 if (isBusy() || stranger) {
382 writeLogMissedIncoming(
383 account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp);
384 if (stranger) {
385 Log.d(
386 Config.LOGTAG,
387 id.account.getJid().asBareJid()
388 + ": ignoring call proposal from stranger "
389 + id.with);
390 return;
391 }
392 final int activeDevices = account.activeDevicesWithRtpCapability();
393 Log.d(Config.LOGTAG, "active devices with rtp capability: " + activeDevices);
394 if (activeDevices == 0) {
395 final MessagePacket reject =
396 mXmppConnectionService
397 .getMessageGenerator()
398 .sessionReject(from, sessionId);
399 mXmppConnectionService.sendMessagePacket(account, reject);
400 } else {
401 Log.d(
402 Config.LOGTAG,
403 id.account.getJid().asBareJid()
404 + ": ignoring proposal because busy on this device but there are other devices");
405 }
406 } else {
407 final JingleRtpConnection rtpConnection =
408 new JingleRtpConnection(this, id, from);
409 this.connections.put(id, rtpConnection);
410 rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
411 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
412 }
413 } else {
414 Log.d(
415 Config.LOGTAG,
416 account.getJid().asBareJid()
417 + ": unable to react to proposed session with "
418 + rtpDescriptions.size()
419 + " rtp descriptions of "
420 + descriptions.size()
421 + " total descriptions");
422 }
423 } else if (addressedDirectly && "proceed".equals(message.getName())) {
424 synchronized (rtpSessionProposals) {
425 final RtpSessionProposal proposal =
426 getRtpSessionProposal(account, from.asBareJid(), sessionId);
427 if (proposal != null) {
428 rtpSessionProposals.remove(proposal);
429 final JingleRtpConnection rtpConnection =
430 new JingleRtpConnection(this, id, account.getJid());
431 rtpConnection.setProposedMedia(proposal.media);
432 this.connections.put(id, rtpConnection);
433 rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED);
434 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
435 } else {
436 Log.d(
437 Config.LOGTAG,
438 account.getJid().asBareJid()
439 + ": no rtp session proposal found for "
440 + from
441 + " to deliver proceed");
442 if (remoteMsgId == null) {
443 return;
444 }
445 final MessagePacket errorMessage = new MessagePacket();
446 errorMessage.setTo(from);
447 errorMessage.setId(remoteMsgId);
448 errorMessage.setType(MessagePacket.TYPE_ERROR);
449 final Element error = errorMessage.addChild("error");
450 error.setAttribute("code", "404");
451 error.setAttribute("type", "cancel");
452 error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
453 mXmppConnectionService.sendMessagePacket(account, errorMessage);
454 }
455 }
456 } else if (addressedDirectly && "reject".equals(message.getName())) {
457 final RtpSessionProposal proposal =
458 getRtpSessionProposal(account, from.asBareJid(), sessionId);
459 synchronized (rtpSessionProposals) {
460 if (proposal != null && rtpSessionProposals.remove(proposal) != null) {
461 writeLogMissedOutgoing(
462 account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
463 toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media);
464 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
465 account,
466 proposal.with,
467 proposal.sessionId,
468 RtpEndUserState.DECLINED_OR_BUSY);
469 } else {
470 Log.d(
471 Config.LOGTAG,
472 account.getJid().asBareJid()
473 + ": no rtp session proposal found for "
474 + from
475 + " to deliver reject");
476 }
477 }
478 } else if (addressedDirectly && "ringing".equals(message.getName())) {
479 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + from + " started ringing");
480 updateProposedSessionDiscovered(
481 account, from, sessionId, DeviceDiscoveryState.DISCOVERED);
482 } else {
483 Log.d(
484 Config.LOGTAG,
485 account.getJid().asBareJid()
486 + ": retrieved out of order jingle message from "
487 + from
488 + message
489 + ", addressedDirectly="
490 + addressedDirectly);
491 }
492 }
493
494 private RtpSessionProposal getRtpSessionProposal(
495 final Account account, Jid from, String sessionId) {
496 for (RtpSessionProposal rtpSessionProposal : rtpSessionProposals.keySet()) {
497 if (rtpSessionProposal.sessionId.equals(sessionId)
498 && rtpSessionProposal.with.equals(from)
499 && rtpSessionProposal.account.getJid().equals(account.getJid())) {
500 return rtpSessionProposal;
501 }
502 }
503 return null;
504 }
505
506 private void writeLogMissedOutgoing(
507 final Account account,
508 Jid with,
509 final String sessionId,
510 String serverMsgId,
511 long timestamp) {
512 final Conversation conversation =
513 mXmppConnectionService.findOrCreateConversation(
514 account, with.asBareJid(), false, false);
515 final Message message =
516 new Message(conversation, Message.STATUS_SEND, Message.TYPE_RTP_SESSION, sessionId);
517 message.setBody(new RtpSessionStatus(false, 0).toString());
518 message.setServerMsgId(serverMsgId);
519 message.setTime(timestamp);
520 writeMessage(message);
521 }
522
523 private void writeLogMissedIncoming(
524 final Account account,
525 Jid with,
526 final String sessionId,
527 String serverMsgId,
528 long timestamp) {
529 final Conversation conversation =
530 mXmppConnectionService.findOrCreateConversation(
531 account, with.asBareJid(), false, false);
532 final Message message =
533 new Message(
534 conversation, Message.STATUS_RECEIVED, Message.TYPE_RTP_SESSION, sessionId);
535 message.setBody(new RtpSessionStatus(false, 0).toString());
536 message.setServerMsgId(serverMsgId);
537 message.setTime(timestamp);
538 writeMessage(message);
539 }
540
541 private void writeMessage(final Message message) {
542 final Conversational conversational = message.getConversation();
543 if (conversational instanceof Conversation) {
544 ((Conversation) conversational).add(message);
545 mXmppConnectionService.databaseBackend.createMessage(message);
546 mXmppConnectionService.updateConversationUi();
547 } else {
548 throw new IllegalStateException("Somehow the conversation in a message was a stub");
549 }
550 }
551
552 public void startJingleFileTransfer(final Message message) {
553 Preconditions.checkArgument(
554 message.isFileOrImage(), "Message is not of type file or image");
555 final Transferable old = message.getTransferable();
556 if (old != null) {
557 old.cancel();
558 }
559 final Account account = message.getConversation().getAccount();
560 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message);
561 final JingleFileTransferConnection connection =
562 new JingleFileTransferConnection(this, id, account.getJid());
563 mXmppConnectionService.markMessage(message, Message.STATUS_WAITING);
564 this.connections.put(id, connection);
565 connection.init(message);
566 }
567
568 public Optional<OngoingRtpSession> getOngoingRtpConnection(final Contact contact) {
569 for (final Map.Entry<AbstractJingleConnection.Id, AbstractJingleConnection> entry :
570 this.connections.entrySet()) {
571 if (entry.getValue() instanceof JingleRtpConnection) {
572 final AbstractJingleConnection.Id id = entry.getKey();
573 if (id.account == contact.getAccount()
574 && id.with.asBareJid().equals(contact.getJid().asBareJid())) {
575 return Optional.of(id);
576 }
577 }
578 }
579 synchronized (this.rtpSessionProposals) {
580 for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
581 this.rtpSessionProposals.entrySet()) {
582 final RtpSessionProposal proposal = entry.getKey();
583 if (proposal.account == contact.getAccount()
584 && contact.getJid().asBareJid().equals(proposal.with)) {
585 final DeviceDiscoveryState preexistingState = entry.getValue();
586 if (preexistingState != null
587 && preexistingState != DeviceDiscoveryState.FAILED) {
588 return Optional.of(proposal);
589 }
590 }
591 }
592 }
593 return Optional.absent();
594 }
595
596 void finishConnection(final AbstractJingleConnection connection) {
597 this.connections.remove(connection.getId());
598 }
599
600 void finishConnectionOrThrow(final AbstractJingleConnection connection) {
601 final AbstractJingleConnection.Id id = connection.getId();
602 if (this.connections.remove(id) == null) {
603 throw new IllegalStateException(
604 String.format("Unable to finish connection with id=%s", id.toString()));
605 }
606 }
607
608 public boolean fireJingleRtpConnectionStateUpdates() {
609 boolean firedUpdates = false;
610 for (final AbstractJingleConnection connection : this.connections.values()) {
611 if (connection instanceof JingleRtpConnection) {
612 final JingleRtpConnection jingleRtpConnection = (JingleRtpConnection) connection;
613 if (jingleRtpConnection.isTerminated()) {
614 continue;
615 }
616 jingleRtpConnection.fireStateUpdate();
617 firedUpdates = true;
618 }
619 }
620 return firedUpdates;
621 }
622
623 void getPrimaryCandidate(
624 final Account account,
625 final boolean initiator,
626 final OnPrimaryCandidateFound listener) {
627 if (Config.DISABLE_PROXY_LOOKUP) {
628 listener.onPrimaryCandidateFound(false, null);
629 return;
630 }
631 if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) {
632 final Jid proxy =
633 account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS);
634 if (proxy != null) {
635 IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
636 iq.setTo(proxy);
637 iq.query(Namespace.BYTE_STREAMS);
638 account.getXmppConnection()
639 .sendIqPacket(
640 iq,
641 new OnIqPacketReceived() {
642
643 @Override
644 public void onIqPacketReceived(
645 Account account, IqPacket packet) {
646 final Element streamhost =
647 packet.query()
648 .findChild(
649 "streamhost",
650 Namespace.BYTE_STREAMS);
651 final String host =
652 streamhost == null
653 ? null
654 : streamhost.getAttribute("host");
655 final String port =
656 streamhost == null
657 ? null
658 : streamhost.getAttribute("port");
659 if (host != null && port != null) {
660 try {
661 JingleCandidate candidate =
662 new JingleCandidate(nextRandomId(), true);
663 candidate.setHost(host);
664 candidate.setPort(Integer.parseInt(port));
665 candidate.setType(JingleCandidate.TYPE_PROXY);
666 candidate.setJid(proxy);
667 candidate.setPriority(
668 655360 + (initiator ? 30 : 0));
669 primaryCandidates.put(
670 account.getJid().asBareJid(), candidate);
671 listener.onPrimaryCandidateFound(true, candidate);
672 } catch (final NumberFormatException e) {
673 listener.onPrimaryCandidateFound(false, null);
674 }
675 } else {
676 listener.onPrimaryCandidateFound(false, null);
677 }
678 }
679 });
680 } else {
681 listener.onPrimaryCandidateFound(false, null);
682 }
683
684 } else {
685 listener.onPrimaryCandidateFound(
686 true, this.primaryCandidates.get(account.getJid().asBareJid()));
687 }
688 }
689
690 public void retractSessionProposal(final Account account, final Jid with) {
691 synchronized (this.rtpSessionProposals) {
692 RtpSessionProposal matchingProposal = null;
693 for (RtpSessionProposal proposal : this.rtpSessionProposals.keySet()) {
694 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
695 matchingProposal = proposal;
696 break;
697 }
698 }
699 if (matchingProposal != null) {
700 retractSessionProposal(matchingProposal);
701 }
702 }
703 }
704
705 private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) {
706 final Account account = rtpSessionProposal.account;
707 toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media);
708 Log.d(
709 Config.LOGTAG,
710 account.getJid().asBareJid()
711 + ": retracting rtp session proposal with "
712 + rtpSessionProposal.with);
713 this.rtpSessionProposals.remove(rtpSessionProposal);
714 final MessagePacket messagePacket =
715 mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
716 writeLogMissedOutgoing(
717 account,
718 rtpSessionProposal.with,
719 rtpSessionProposal.sessionId,
720 null,
721 System.currentTimeMillis());
722 mXmppConnectionService.sendMessagePacket(account, messagePacket);
723 }
724
725 public String initializeRtpSession(
726 final Account account, final Jid with, final Set<Media> media) {
727 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
728 final JingleRtpConnection rtpConnection =
729 new JingleRtpConnection(this, id, account.getJid());
730 rtpConnection.setProposedMedia(media);
731 this.connections.put(id, rtpConnection);
732 rtpConnection.sendSessionInitiate();
733 return id.sessionId;
734 }
735
736 public void proposeJingleRtpSession(
737 final Account account, final Jid with, final Set<Media> media) {
738 synchronized (this.rtpSessionProposals) {
739 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
740 this.rtpSessionProposals.entrySet()) {
741 RtpSessionProposal proposal = entry.getKey();
742 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
743 final DeviceDiscoveryState preexistingState = entry.getValue();
744 if (preexistingState != null
745 && preexistingState != DeviceDiscoveryState.FAILED) {
746 final RtpEndUserState endUserState = preexistingState.toEndUserState();
747 toneManager.transition(endUserState, media);
748 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
749 account, with, proposal.sessionId, endUserState);
750 return;
751 }
752 }
753 }
754 if (isBusy()) {
755 if (hasMatchingRtpSession(account, with, media)) {
756 Log.d(
757 Config.LOGTAG,
758 "ignoring request to propose jingle session because the other party already created one for us");
759 return;
760 }
761 throw new IllegalStateException(
762 "There is already a running RTP session. This should have been caught by the UI");
763 }
764 final RtpSessionProposal proposal =
765 RtpSessionProposal.of(account, with.asBareJid(), media);
766 this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
767 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
768 account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE);
769 final MessagePacket messagePacket =
770 mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
771 mXmppConnectionService.sendMessagePacket(account, messagePacket);
772 }
773 }
774
775 public boolean hasMatchingProposal(final Account account, final Jid with) {
776 synchronized (this.rtpSessionProposals) {
777 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
778 this.rtpSessionProposals.entrySet()) {
779 final RtpSessionProposal proposal = entry.getKey();
780 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
781 return true;
782 }
783 }
784 }
785 return false;
786 }
787
788 public void deliverIbbPacket(Account account, IqPacket packet) {
789 final String sid;
790 final Element payload;
791 if (packet.hasChild("open", Namespace.IBB)) {
792 payload = packet.findChild("open", Namespace.IBB);
793 sid = payload.getAttribute("sid");
794 } else if (packet.hasChild("data", Namespace.IBB)) {
795 payload = packet.findChild("data", Namespace.IBB);
796 sid = payload.getAttribute("sid");
797 } else if (packet.hasChild("close", Namespace.IBB)) {
798 payload = packet.findChild("close", Namespace.IBB);
799 sid = payload.getAttribute("sid");
800 } else {
801 payload = null;
802 sid = null;
803 }
804 if (sid != null) {
805 for (final AbstractJingleConnection connection : this.connections.values()) {
806 if (connection instanceof JingleFileTransferConnection) {
807 final JingleFileTransferConnection fileTransfer =
808 (JingleFileTransferConnection) connection;
809 final JingleTransport transport = fileTransfer.getTransport();
810 if (transport instanceof JingleInBandTransport) {
811 final JingleInBandTransport inBandTransport =
812 (JingleInBandTransport) transport;
813 if (inBandTransport.matches(account, sid)) {
814 inBandTransport.deliverPayload(packet, payload);
815 }
816 return;
817 }
818 }
819 }
820 }
821 Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString());
822 account.getXmppConnection()
823 .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
824 }
825
826 public void notifyRebound(final Account account) {
827 for (final AbstractJingleConnection connection : this.connections.values()) {
828 connection.notifyRebound();
829 }
830 final XmppConnection xmppConnection = account.getXmppConnection();
831 if (xmppConnection != null && xmppConnection.getFeatures().sm()) {
832 resendSessionProposals(account);
833 }
834 }
835
836 public WeakReference<JingleRtpConnection> findJingleRtpConnection(
837 Account account, Jid with, String sessionId) {
838 final AbstractJingleConnection.Id id =
839 AbstractJingleConnection.Id.of(account, with, sessionId);
840 final AbstractJingleConnection connection = connections.get(id);
841 if (connection instanceof JingleRtpConnection) {
842 return new WeakReference<>((JingleRtpConnection) connection);
843 }
844 return null;
845 }
846
847 private void resendSessionProposals(final Account account) {
848 synchronized (this.rtpSessionProposals) {
849 for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
850 this.rtpSessionProposals.entrySet()) {
851 final RtpSessionProposal proposal = entry.getKey();
852 if (entry.getValue() == DeviceDiscoveryState.SEARCHING
853 && proposal.account == account) {
854 Log.d(
855 Config.LOGTAG,
856 account.getJid().asBareJid()
857 + ": resending session proposal to "
858 + proposal.with);
859 final MessagePacket messagePacket =
860 mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
861 mXmppConnectionService.sendMessagePacket(account, messagePacket);
862 }
863 }
864 }
865 }
866
867 public void updateProposedSessionDiscovered(
868 Account account, Jid from, String sessionId, final DeviceDiscoveryState target) {
869 synchronized (this.rtpSessionProposals) {
870 final RtpSessionProposal sessionProposal =
871 getRtpSessionProposal(account, from.asBareJid(), sessionId);
872 final DeviceDiscoveryState currentState =
873 sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal);
874 if (currentState == null) {
875 Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId);
876 return;
877 }
878 if (currentState == DeviceDiscoveryState.DISCOVERED) {
879 Log.d(
880 Config.LOGTAG,
881 "session proposal already at discovered. not going to fall back");
882 return;
883 }
884 this.rtpSessionProposals.put(sessionProposal, target);
885 final RtpEndUserState endUserState = target.toEndUserState();
886 toneManager.transition(endUserState, sessionProposal.media);
887 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
888 account, sessionProposal.with, sessionProposal.sessionId, endUserState);
889 Log.d(
890 Config.LOGTAG,
891 account.getJid().asBareJid()
892 + ": flagging session "
893 + sessionId
894 + " as "
895 + target);
896 }
897 }
898
899 public void rejectRtpSession(final String sessionId) {
900 for (final AbstractJingleConnection connection : this.connections.values()) {
901 if (connection.getId().sessionId.equals(sessionId)) {
902 if (connection instanceof JingleRtpConnection) {
903 try {
904 ((JingleRtpConnection) connection).rejectCall();
905 return;
906 } catch (final IllegalStateException e) {
907 Log.w(
908 Config.LOGTAG,
909 "race condition on rejecting call from notification",
910 e);
911 }
912 }
913 }
914 }
915 }
916
917 public void endRtpSession(final String sessionId) {
918 for (final AbstractJingleConnection connection : this.connections.values()) {
919 if (connection.getId().sessionId.equals(sessionId)) {
920 if (connection instanceof JingleRtpConnection) {
921 ((JingleRtpConnection) connection).endCall();
922 }
923 }
924 }
925 }
926
927 public void failProceed(Account account, final Jid with, final String sessionId, final String message) {
928 final AbstractJingleConnection.Id id =
929 AbstractJingleConnection.Id.of(account, with, sessionId);
930 final AbstractJingleConnection existingJingleConnection = connections.get(id);
931 if (existingJingleConnection instanceof JingleRtpConnection) {
932 ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(message);
933 }
934 }
935
936 void ensureConnectionIsRegistered(final AbstractJingleConnection connection) {
937 if (connections.containsValue(connection)) {
938 return;
939 }
940 final IllegalStateException e =
941 new IllegalStateException(
942 "JingleConnection has not been registered with connection manager");
943 Log.e(Config.LOGTAG, "ensureConnectionIsRegistered() failed. Going to throw", e);
944 throw e;
945 }
946
947 void setTerminalSessionState(
948 AbstractJingleConnection.Id id, final RtpEndUserState state, final Set<Media> media) {
949 this.terminatedSessions.put(
950 PersistableSessionId.of(id), new TerminatedRtpSession(state, media));
951 }
952
953 public TerminatedRtpSession getTerminalSessionState(final Jid with, final String sessionId) {
954 return this.terminatedSessions.getIfPresent(new PersistableSessionId(with, sessionId));
955 }
956
957 private static class PersistableSessionId {
958 private final Jid with;
959 private final String sessionId;
960
961 private PersistableSessionId(Jid with, String sessionId) {
962 this.with = with;
963 this.sessionId = sessionId;
964 }
965
966 public static PersistableSessionId of(AbstractJingleConnection.Id id) {
967 return new PersistableSessionId(id.with, id.sessionId);
968 }
969
970 @Override
971 public boolean equals(Object o) {
972 if (this == o) return true;
973 if (o == null || getClass() != o.getClass()) return false;
974 PersistableSessionId that = (PersistableSessionId) o;
975 return Objects.equal(with, that.with) && Objects.equal(sessionId, that.sessionId);
976 }
977
978 @Override
979 public int hashCode() {
980 return Objects.hashCode(with, sessionId);
981 }
982 }
983
984 public static class TerminatedRtpSession {
985 public final RtpEndUserState state;
986 public final Set<Media> media;
987
988 TerminatedRtpSession(RtpEndUserState state, Set<Media> media) {
989 this.state = state;
990 this.media = media;
991 }
992 }
993
994 public enum DeviceDiscoveryState {
995 SEARCHING,
996 SEARCHING_ACKNOWLEDGED,
997 DISCOVERED,
998 FAILED;
999
1000 public RtpEndUserState toEndUserState() {
1001 switch (this) {
1002 case SEARCHING:
1003 case SEARCHING_ACKNOWLEDGED:
1004 return RtpEndUserState.FINDING_DEVICE;
1005 case DISCOVERED:
1006 return RtpEndUserState.RINGING;
1007 default:
1008 return RtpEndUserState.CONNECTIVITY_ERROR;
1009 }
1010 }
1011 }
1012
1013 public static class RtpSessionProposal implements OngoingRtpSession {
1014 public final Jid with;
1015 public final String sessionId;
1016 public final Set<Media> media;
1017 private final Account account;
1018
1019 private RtpSessionProposal(Account account, Jid with, String sessionId) {
1020 this(account, with, sessionId, Collections.emptySet());
1021 }
1022
1023 private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media) {
1024 this.account = account;
1025 this.with = with;
1026 this.sessionId = sessionId;
1027 this.media = media;
1028 }
1029
1030 public static RtpSessionProposal of(Account account, Jid with, Set<Media> media) {
1031 return new RtpSessionProposal(account, with, nextRandomId(), media);
1032 }
1033
1034 @Override
1035 public boolean equals(Object o) {
1036 if (this == o) return true;
1037 if (o == null || getClass() != o.getClass()) return false;
1038 RtpSessionProposal proposal = (RtpSessionProposal) o;
1039 return Objects.equal(account.getJid(), proposal.account.getJid())
1040 && Objects.equal(with, proposal.with)
1041 && Objects.equal(sessionId, proposal.sessionId);
1042 }
1043
1044 @Override
1045 public int hashCode() {
1046 return Objects.hashCode(account.getJid(), with, sessionId);
1047 }
1048
1049 @Override
1050 public Account getAccount() {
1051 return account;
1052 }
1053
1054 @Override
1055 public Jid getWith() {
1056 return with;
1057 }
1058
1059 @Override
1060 public String getSessionId() {
1061 return sessionId;
1062 }
1063 }
1064}