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