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