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 // update chat UI to remove 'ongoing call' icon
651 mXmppConnectionService.updateConversationUi();
652 }
653
654 public boolean fireJingleRtpConnectionStateUpdates() {
655 boolean firedUpdates = false;
656 for (final AbstractJingleConnection connection : this.connections.values()) {
657 if (connection instanceof JingleRtpConnection) {
658 final JingleRtpConnection jingleRtpConnection = (JingleRtpConnection) connection;
659 if (jingleRtpConnection.isTerminated()) {
660 continue;
661 }
662 jingleRtpConnection.fireStateUpdate();
663 firedUpdates = true;
664 }
665 }
666 return firedUpdates;
667 }
668
669 void getPrimaryCandidate(
670 final Account account,
671 final boolean initiator,
672 final OnPrimaryCandidateFound listener) {
673 if (Config.DISABLE_PROXY_LOOKUP) {
674 listener.onPrimaryCandidateFound(false, null);
675 return;
676 }
677 if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) {
678 final Jid proxy =
679 account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS);
680 if (proxy != null) {
681 IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
682 iq.setTo(proxy);
683 iq.query(Namespace.BYTE_STREAMS);
684 account.getXmppConnection()
685 .sendIqPacket(
686 iq,
687 new OnIqPacketReceived() {
688
689 @Override
690 public void onIqPacketReceived(
691 Account account, IqPacket packet) {
692 final Element streamhost =
693 packet.query()
694 .findChild(
695 "streamhost",
696 Namespace.BYTE_STREAMS);
697 final String host =
698 streamhost == null
699 ? null
700 : streamhost.getAttribute("host");
701 final String port =
702 streamhost == null
703 ? null
704 : streamhost.getAttribute("port");
705 if (host != null && port != null) {
706 try {
707 JingleCandidate candidate =
708 new JingleCandidate(nextRandomId(), true);
709 candidate.setHost(host);
710 candidate.setPort(Integer.parseInt(port));
711 candidate.setType(JingleCandidate.TYPE_PROXY);
712 candidate.setJid(proxy);
713 candidate.setPriority(
714 655360 + (initiator ? 30 : 0));
715 primaryCandidates.put(
716 account.getJid().asBareJid(), candidate);
717 listener.onPrimaryCandidateFound(true, candidate);
718 } catch (final NumberFormatException e) {
719 listener.onPrimaryCandidateFound(false, null);
720 }
721 } else {
722 listener.onPrimaryCandidateFound(false, null);
723 }
724 }
725 });
726 } else {
727 listener.onPrimaryCandidateFound(false, null);
728 }
729
730 } else {
731 listener.onPrimaryCandidateFound(
732 true, this.primaryCandidates.get(account.getJid().asBareJid()));
733 }
734 }
735
736 public void retractSessionProposal(final Account account, final Jid with) {
737 synchronized (this.rtpSessionProposals) {
738 RtpSessionProposal matchingProposal = null;
739 for (RtpSessionProposal proposal : this.rtpSessionProposals.keySet()) {
740 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
741 matchingProposal = proposal;
742 break;
743 }
744 }
745 if (matchingProposal != null) {
746 retractSessionProposal(matchingProposal);
747 }
748 }
749 }
750
751 private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) {
752 final Account account = rtpSessionProposal.account;
753 toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media);
754 Log.d(
755 Config.LOGTAG,
756 account.getJid().asBareJid()
757 + ": retracting rtp session proposal with "
758 + rtpSessionProposal.with);
759 this.rtpSessionProposals.remove(rtpSessionProposal);
760 final MessagePacket messagePacket =
761 mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
762 writeLogMissedOutgoing(
763 account,
764 rtpSessionProposal.with,
765 rtpSessionProposal.sessionId,
766 null,
767 System.currentTimeMillis());
768 mXmppConnectionService.sendMessagePacket(account, messagePacket);
769 }
770
771 public String initializeRtpSession(
772 final Account account, final Jid with, final Set<Media> media) {
773 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
774 final JingleRtpConnection rtpConnection =
775 new JingleRtpConnection(this, id, account.getJid());
776 rtpConnection.setProposedMedia(media);
777 this.connections.put(id, rtpConnection);
778 rtpConnection.sendSessionInitiate();
779 return id.sessionId;
780 }
781
782 public void proposeJingleRtpSession(
783 final Account account, final Jid with, final Set<Media> media) {
784 synchronized (this.rtpSessionProposals) {
785 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
786 this.rtpSessionProposals.entrySet()) {
787 RtpSessionProposal proposal = entry.getKey();
788 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
789 final DeviceDiscoveryState preexistingState = entry.getValue();
790 if (preexistingState != null
791 && preexistingState != DeviceDiscoveryState.FAILED) {
792 final RtpEndUserState endUserState = preexistingState.toEndUserState();
793 toneManager.transition(endUserState, media);
794 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
795 account, with, proposal.sessionId, endUserState);
796 return;
797 }
798 }
799 }
800 if (isBusy()) {
801 if (hasMatchingRtpSession(account, with, media)) {
802 Log.d(
803 Config.LOGTAG,
804 "ignoring request to propose jingle session because the other party already created one for us");
805 return;
806 }
807 throw new IllegalStateException(
808 "There is already a running RTP session. This should have been caught by the UI");
809 }
810 final RtpSessionProposal proposal =
811 RtpSessionProposal.of(account, with.asBareJid(), media);
812 this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
813 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
814 account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE);
815 final MessagePacket messagePacket =
816 mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
817 mXmppConnectionService.sendMessagePacket(account, messagePacket);
818 }
819 }
820
821 public boolean hasMatchingProposal(final Account account, final Jid with) {
822 synchronized (this.rtpSessionProposals) {
823 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
824 this.rtpSessionProposals.entrySet()) {
825 final RtpSessionProposal proposal = entry.getKey();
826 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
827 return true;
828 }
829 }
830 }
831 return false;
832 }
833
834 public void deliverIbbPacket(Account account, IqPacket packet) {
835 final String sid;
836 final Element payload;
837 if (packet.hasChild("open", Namespace.IBB)) {
838 payload = packet.findChild("open", Namespace.IBB);
839 sid = payload.getAttribute("sid");
840 } else if (packet.hasChild("data", Namespace.IBB)) {
841 payload = packet.findChild("data", Namespace.IBB);
842 sid = payload.getAttribute("sid");
843 } else if (packet.hasChild("close", Namespace.IBB)) {
844 payload = packet.findChild("close", Namespace.IBB);
845 sid = payload.getAttribute("sid");
846 } else {
847 payload = null;
848 sid = null;
849 }
850 if (sid != null) {
851 for (final AbstractJingleConnection connection : this.connections.values()) {
852 if (connection instanceof JingleFileTransferConnection) {
853 final JingleFileTransferConnection fileTransfer =
854 (JingleFileTransferConnection) connection;
855 final JingleTransport transport = fileTransfer.getTransport();
856 if (transport instanceof JingleInBandTransport) {
857 final JingleInBandTransport inBandTransport =
858 (JingleInBandTransport) transport;
859 if (inBandTransport.matches(account, sid)) {
860 inBandTransport.deliverPayload(packet, payload);
861 }
862 return;
863 }
864 }
865 }
866 }
867 Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString());
868 account.getXmppConnection()
869 .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
870 }
871
872 public void notifyRebound(final Account account) {
873 for (final AbstractJingleConnection connection : this.connections.values()) {
874 connection.notifyRebound();
875 }
876 final XmppConnection xmppConnection = account.getXmppConnection();
877 if (xmppConnection != null && xmppConnection.getFeatures().sm()) {
878 resendSessionProposals(account);
879 }
880 }
881
882 public WeakReference<JingleRtpConnection> findJingleRtpConnection(
883 Account account, Jid with, String sessionId) {
884 final AbstractJingleConnection.Id id =
885 AbstractJingleConnection.Id.of(account, with, sessionId);
886 final AbstractJingleConnection connection = connections.get(id);
887 if (connection instanceof JingleRtpConnection) {
888 return new WeakReference<>((JingleRtpConnection) connection);
889 }
890 return null;
891 }
892
893 private void resendSessionProposals(final Account account) {
894 synchronized (this.rtpSessionProposals) {
895 for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
896 this.rtpSessionProposals.entrySet()) {
897 final RtpSessionProposal proposal = entry.getKey();
898 if (entry.getValue() == DeviceDiscoveryState.SEARCHING
899 && proposal.account == account) {
900 Log.d(
901 Config.LOGTAG,
902 account.getJid().asBareJid()
903 + ": resending session proposal to "
904 + proposal.with);
905 final MessagePacket messagePacket =
906 mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
907 mXmppConnectionService.sendMessagePacket(account, messagePacket);
908 }
909 }
910 }
911 }
912
913 public void updateProposedSessionDiscovered(
914 Account account, Jid from, String sessionId, final DeviceDiscoveryState target) {
915 synchronized (this.rtpSessionProposals) {
916 final RtpSessionProposal sessionProposal =
917 getRtpSessionProposal(account, from.asBareJid(), sessionId);
918 final DeviceDiscoveryState currentState =
919 sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal);
920 if (currentState == null) {
921 Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId);
922 return;
923 }
924 if (currentState == DeviceDiscoveryState.DISCOVERED) {
925 Log.d(
926 Config.LOGTAG,
927 "session proposal already at discovered. not going to fall back");
928 return;
929 }
930 this.rtpSessionProposals.put(sessionProposal, target);
931 final RtpEndUserState endUserState = target.toEndUserState();
932 toneManager.transition(endUserState, sessionProposal.media);
933 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
934 account, sessionProposal.with, sessionProposal.sessionId, endUserState);
935 Log.d(
936 Config.LOGTAG,
937 account.getJid().asBareJid()
938 + ": flagging session "
939 + sessionId
940 + " as "
941 + target);
942 }
943 }
944
945 public void rejectRtpSession(final String sessionId) {
946 for (final AbstractJingleConnection connection : this.connections.values()) {
947 if (connection.getId().sessionId.equals(sessionId)) {
948 if (connection instanceof JingleRtpConnection) {
949 try {
950 ((JingleRtpConnection) connection).rejectCall();
951 return;
952 } catch (final IllegalStateException e) {
953 Log.w(
954 Config.LOGTAG,
955 "race condition on rejecting call from notification",
956 e);
957 }
958 }
959 }
960 }
961 }
962
963 public void endRtpSession(final String sessionId) {
964 for (final AbstractJingleConnection connection : this.connections.values()) {
965 if (connection.getId().sessionId.equals(sessionId)) {
966 if (connection instanceof JingleRtpConnection) {
967 ((JingleRtpConnection) connection).endCall();
968 }
969 }
970 }
971 }
972
973 public void failProceed(Account account, final Jid with, final String sessionId, final String message) {
974 final AbstractJingleConnection.Id id =
975 AbstractJingleConnection.Id.of(account, with, sessionId);
976 final AbstractJingleConnection existingJingleConnection = connections.get(id);
977 if (existingJingleConnection instanceof JingleRtpConnection) {
978 ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(message);
979 }
980 }
981
982 void ensureConnectionIsRegistered(final AbstractJingleConnection connection) {
983 if (connections.containsValue(connection)) {
984 return;
985 }
986 final IllegalStateException e =
987 new IllegalStateException(
988 "JingleConnection has not been registered with connection manager");
989 Log.e(Config.LOGTAG, "ensureConnectionIsRegistered() failed. Going to throw", e);
990 throw e;
991 }
992
993 void setTerminalSessionState(
994 AbstractJingleConnection.Id id, final RtpEndUserState state, final Set<Media> media) {
995 this.terminatedSessions.put(
996 PersistableSessionId.of(id), new TerminatedRtpSession(state, media));
997 }
998
999 public TerminatedRtpSession getTerminalSessionState(final Jid with, final String sessionId) {
1000 return this.terminatedSessions.getIfPresent(new PersistableSessionId(with, sessionId));
1001 }
1002
1003 private static class PersistableSessionId {
1004 private final Jid with;
1005 private final String sessionId;
1006
1007 private PersistableSessionId(Jid with, String sessionId) {
1008 this.with = with;
1009 this.sessionId = sessionId;
1010 }
1011
1012 public static PersistableSessionId of(AbstractJingleConnection.Id id) {
1013 return new PersistableSessionId(id.with, id.sessionId);
1014 }
1015
1016 @Override
1017 public boolean equals(Object o) {
1018 if (this == o) return true;
1019 if (o == null || getClass() != o.getClass()) return false;
1020 PersistableSessionId that = (PersistableSessionId) o;
1021 return Objects.equal(with, that.with) && Objects.equal(sessionId, that.sessionId);
1022 }
1023
1024 @Override
1025 public int hashCode() {
1026 return Objects.hashCode(with, sessionId);
1027 }
1028 }
1029
1030 public static class TerminatedRtpSession {
1031 public final RtpEndUserState state;
1032 public final Set<Media> media;
1033
1034 TerminatedRtpSession(RtpEndUserState state, Set<Media> media) {
1035 this.state = state;
1036 this.media = media;
1037 }
1038 }
1039
1040 public enum DeviceDiscoveryState {
1041 SEARCHING,
1042 SEARCHING_ACKNOWLEDGED,
1043 DISCOVERED,
1044 FAILED;
1045
1046 public RtpEndUserState toEndUserState() {
1047 switch (this) {
1048 case SEARCHING:
1049 case SEARCHING_ACKNOWLEDGED:
1050 return RtpEndUserState.FINDING_DEVICE;
1051 case DISCOVERED:
1052 return RtpEndUserState.RINGING;
1053 default:
1054 return RtpEndUserState.CONNECTIVITY_ERROR;
1055 }
1056 }
1057 }
1058
1059 public static class RtpSessionProposal implements OngoingRtpSession {
1060 public final Jid with;
1061 public final String sessionId;
1062 public final Set<Media> media;
1063 private final Account account;
1064
1065 private RtpSessionProposal(Account account, Jid with, String sessionId) {
1066 this(account, with, sessionId, Collections.emptySet());
1067 }
1068
1069 private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media) {
1070 this.account = account;
1071 this.with = with;
1072 this.sessionId = sessionId;
1073 this.media = media;
1074 }
1075
1076 public static RtpSessionProposal of(Account account, Jid with, Set<Media> media) {
1077 return new RtpSessionProposal(account, with, nextRandomId(), media);
1078 }
1079
1080 @Override
1081 public boolean equals(Object o) {
1082 if (this == o) return true;
1083 if (o == null || getClass() != o.getClass()) return false;
1084 RtpSessionProposal proposal = (RtpSessionProposal) o;
1085 return Objects.equal(account.getJid(), proposal.account.getJid())
1086 && Objects.equal(with, proposal.with)
1087 && Objects.equal(sessionId, proposal.sessionId);
1088 }
1089
1090 @Override
1091 public int hashCode() {
1092 return Objects.hashCode(account.getJid(), with, sessionId);
1093 }
1094
1095 @Override
1096 public Account getAccount() {
1097 return account;
1098 }
1099
1100 @Override
1101 public Jid getWith() {
1102 return with;
1103 }
1104
1105 @Override
1106 public String getSessionId() {
1107 return sessionId;
1108 }
1109 }
1110}