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