From 1f772df74fee6a6b4cc840a07530dc2ab5347911 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 25 Feb 2022 16:24:13 +0100 Subject: [PATCH] remove security check that ensures rtp connection was properly finished this only causes race conditions --- .../conversations/ui/RtpSessionActivity.java | 1 - .../xmpp/jingle/AbstractJingleConnection.java | 3 + .../xmpp/jingle/JingleConnectionManager.java | 533 ++++--- .../xmpp/jingle/JingleRtpConnection.java | 1312 +++++++++++------ 4 files changed, 1239 insertions(+), 610 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index a27794c30..6320ef5b7 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -646,7 +646,6 @@ public class RtpSessionActivity extends XmppActivity final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); final boolean verified = requireRtpConnection().isVerified(); if (currentState == RtpEndUserState.ENDED) { - reference.get().throwStateTransitionException(); finish(); return true; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 90f06fe26..d719c729e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp.jingle; +import androidx.annotation.NonNull; + import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; @@ -110,6 +112,7 @@ public abstract class AbstractJingleConnection { } @Override + @NonNull public String toString() { return MoreObjects.toStringHelper(this) .add("account", account.getJid()) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index cbf4b85fd..533974a0f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -52,14 +52,16 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class JingleConnectionManager extends AbstractConnectionManager { - static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); + static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = + Executors.newSingleThreadScheduledExecutor(); final ToneManager toneManager; - private final HashMap rtpSessionProposals = new HashMap<>(); - private final ConcurrentHashMap connections = new ConcurrentHashMap<>(); + private final HashMap rtpSessionProposals = + new HashMap<>(); + private final ConcurrentHashMap + connections = new ConcurrentHashMap<>(); - private final Cache terminatedSessions = CacheBuilder.newBuilder() - .expireAfterWrite(24, TimeUnit.HOURS) - .build(); + private final Cache terminatedSessions = + CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build(); private final HashMap primaryCandidates = new HashMap<>(); @@ -87,17 +89,31 @@ public class JingleConnectionManager extends AbstractConnectionManager { } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) { final Jid from = packet.getFrom(); final Content content = packet.getJingleContent(); - final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace(); + final String descriptionNamespace = + content == null ? null : content.getDescriptionNamespace(); final AbstractJingleConnection connection; if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { connection = new JingleFileTransferConnection(this, id, from); - } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) && isUsingClearNet(account)) { - final boolean sessionEnded = this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id)); - final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with); + } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) + && isUsingClearNet(account)) { + final boolean sessionEnded = + this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id)); + final boolean stranger = + isWithStrangerAndStrangerNotificationsAreOff(account, id.with); if (isBusy() || sessionEnded || stranger) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejected session with " + id.with + " because busy. sessionEnded=" + sessionEnded + ", stranger=" + stranger); - mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); - final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": rejected session with " + + id.with + + " because busy. sessionEnded=" + + sessionEnded + + ", stranger=" + + stranger); + mXmppConnectionService.sendIqPacket( + account, packet.generateResponse(IqPacket.TYPE.RESULT), null); + final JinglePacket sessionTermination = + new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); sessionTermination.setTo(id.with); sessionTermination.setReason(Reason.BUSY, null); mXmppConnectionService.sendIqPacket(account, sessionTermination, null); @@ -105,7 +121,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { } connection = new JingleRtpConnection(this, id, from); } else { - respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel"); + respondWithJingleError( + account, packet, "unsupported-info", "feature-not-implemented", "cancel"); return; } connections.put(id, connection); @@ -136,7 +153,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { synchronized (this.rtpSessionProposals) { return this.rtpSessionProposals.containsValue(DeviceDiscoveryState.DISCOVERED) || this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING) - || this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED); + || this.rtpSessionProposals.containsValue( + DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED); } } @@ -152,14 +170,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - private Optional findMatchingSessionProposal(final Account account, final Jid with, final Set media) { + private Optional findMatchingSessionProposal( + final Account account, final Jid with, final Set media) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { final RtpSessionProposal proposal = entry.getKey(); final DeviceDiscoveryState state = entry.getValue(); - final boolean openProposal = state == DeviceDiscoveryState.DISCOVERED - || state == DeviceDiscoveryState.SEARCHING - || state == DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED; + final boolean openProposal = + state == DeviceDiscoveryState.DISCOVERED + || state == DeviceDiscoveryState.SEARCHING + || state == DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED; if (openProposal && proposal.account == account && proposal.with.equals(with.asBareJid()) @@ -171,7 +192,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { return Optional.absent(); } - private boolean hasMatchingRtpSession(final Account account, final Jid with, final Set media) { + private boolean hasMatchingRtpSession( + final Account account, final Jid with, final Set media) { for (AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleRtpConnection) { final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection; @@ -189,7 +211,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { } private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) { - final boolean notifyForStrangers = mXmppConnectionService.getNotificationService().notificationsFromStrangers(); + final boolean notifyForStrangers = + mXmppConnectionService.getNotificationService().notificationsFromStrangers(); if (notifyForStrangers) { return false; } @@ -197,11 +220,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { return !contact.showInContactList(); } - ScheduledFuture schedule(final Runnable runnable, final long delay, final TimeUnit timeUnit) { + ScheduledFuture schedule( + final Runnable runnable, final long delay, final TimeUnit timeUnit) { return SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, timeUnit); } - void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { + void respondWithJingleError( + final Account account, + final IqPacket original, + String jingleCondition, + String condition, + String conditionType) { final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", conditionType); @@ -210,7 +239,14 @@ public class JingleConnectionManager extends AbstractConnectionManager { account.getXmppConnection().sendIqPacket(response, null); } - public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message, String remoteMsgId, String serverMsgId, long timestamp) { + public void deliverMessage( + final Account account, + final Jid to, + final Jid from, + final Element message, + String remoteMsgId, + String serverMsgId, + long timestamp) { Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace())); final String sessionId = message.getAttribute("id"); if (sessionId == null) { @@ -244,16 +280,24 @@ public class JingleConnectionManager extends AbstractConnectionManager { final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { if (existingJingleConnection instanceof JingleRtpConnection) { - ((JingleRtpConnection) existingJingleConnection).deliveryMessage(from, message, serverMsgId, timestamp); + ((JingleRtpConnection) existingJingleConnection) + .deliveryMessage(from, message, serverMsgId, timestamp); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + existingJingleConnection.getClass().getName() + " does not support jingle messages"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": " + + existingJingleConnection.getClass().getName() + + " does not support jingle messages"); } return; } if (fromSelf) { if ("proceed".equals(message.getName())) { - final Conversation c = mXmppConnectionService.findOrCreateConversation(account, id.with, false, false); + final Conversation c = + mXmppConnectionService.findOrCreateConversation( + account, id.with, false, false); final Message previousBusy = c.findRtpSession(sessionId, Message.STATUS_RECEIVED); if (previousBusy != null) { previousBusy.setBody(new RtpSessionStatus(true, 0).toString()); @@ -262,84 +306,138 @@ public class JingleConnectionManager extends AbstractConnectionManager { } previousBusy.setTime(timestamp); mXmppConnectionService.updateMessage(previousBusy, true); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": updated previous busy because call got picked up by another device"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": updated previous busy because call got picked up by another device"); return; } } - //TODO handle reject for cases where we don’t have carbon copies (normally reject is to be sent to own bare jid as well) - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self"); + // TODO handle reject for cases where we don’t have carbon copies (normally reject is to + // be sent to own bare jid as well) + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": ignore jingle message from self"); return; } if ("propose".equals(message.getName())) { final Propose propose = Propose.upgrade(message); final List descriptions = propose.getDescriptions(); - final Collection rtpDescriptions = Collections2.transform( - Collections2.filter(descriptions, d -> d instanceof RtpDescription), - input -> (RtpDescription) input - ); - if (rtpDescriptions.size() > 0 && rtpDescriptions.size() == descriptions.size() && isUsingClearNet(account)) { - final Collection media = Collections2.transform(rtpDescriptions, RtpDescription::getMedia); + final Collection rtpDescriptions = + Collections2.transform( + Collections2.filter(descriptions, d -> d instanceof RtpDescription), + input -> (RtpDescription) input); + if (rtpDescriptions.size() > 0 + && rtpDescriptions.size() == descriptions.size() + && isUsingClearNet(account)) { + final Collection media = + Collections2.transform(rtpDescriptions, RtpDescription::getMedia); if (media.contains(Media.UNKNOWN)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered unknown media in session proposal. " + propose); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": encountered unknown media in session proposal. " + + propose); return; } - final Optional matchingSessionProposal = findMatchingSessionProposal(account, id.with, ImmutableSet.copyOf(media)); + final Optional matchingSessionProposal = + findMatchingSessionProposal(account, id.with, ImmutableSet.copyOf(media)); if (matchingSessionProposal.isPresent()) { final String ourSessionId = matchingSessionProposal.get().sessionId; final String theirSessionId = id.sessionId; if (ComparisonChain.start() - .compare(ourSessionId, theirSessionId) - .compare(account.getJid().toEscapedString(), id.with.toEscapedString()) - .result() > 0) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": our session lost tie break. automatically accepting their session. winning Session=" + theirSessionId); - //TODO a retract for this reason should probably include some indication of tie break + .compare(ourSessionId, theirSessionId) + .compare( + account.getJid().toEscapedString(), + id.with.toEscapedString()) + .result() + > 0) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": our session lost tie break. automatically accepting their session. winning Session=" + + theirSessionId); + // TODO a retract for this reason should probably include some indication of + // tie break retractSessionProposal(matchingSessionProposal.get()); - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, from); this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": our session won tie break. waiting for other party to accept. winningSession=" + ourSessionId); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": our session won tie break. waiting for other party to accept. winningSession=" + + ourSessionId); } return; } - final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with); + final boolean stranger = + isWithStrangerAndStrangerNotificationsAreOff(account, id.with); if (isBusy() || stranger) { - writeLogMissedIncoming(account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp); + writeLogMissedIncoming( + account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp); if (stranger) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring call proposal from stranger " + id.with); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring call proposal from stranger " + + id.with); return; } final int activeDevices = account.activeDevicesWithRtpCapability(); Log.d(Config.LOGTAG, "active devices with rtp capability: " + activeDevices); if (activeDevices == 0) { - final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId); + final MessagePacket reject = + mXmppConnectionService + .getMessageGenerator() + .sessionReject(from, sessionId); mXmppConnectionService.sendMessagePacket(account, reject); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring proposal because busy on this device but there are other devices"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring proposal because busy on this device but there are other devices"); } } else { - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, from); this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed session with " + rtpDescriptions.size() + " rtp descriptions of " + descriptions.size() + " total descriptions"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to react to proposed session with " + + rtpDescriptions.size() + + " rtp descriptions of " + + descriptions.size() + + " total descriptions"); } } else if (addressedDirectly && "proceed".equals(message.getName())) { synchronized (rtpSessionProposals) { - final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); + final RtpSessionProposal proposal = + getRtpSessionProposal(account, from.asBareJid(), sessionId); if (proposal != null) { rtpSessionProposals.remove(proposal); - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, account.getJid()); rtpConnection.setProposedMedia(proposal.media); this.connections.put(id, rtpConnection); rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver proceed"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": no rtp session proposal found for " + + from + + " to deliver proceed"); if (remoteMsgId == null) { return; } @@ -355,63 +453,77 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } } else if (addressedDirectly && "reject".equals(message.getName())) { - final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); + final RtpSessionProposal proposal = + getRtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { if (proposal != null && rtpSessionProposals.remove(proposal) != null) { - writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp); + writeLogMissedOutgoing( + account, proposal.with, proposal.sessionId, serverMsgId, timestamp); toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media); - mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY); + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, + proposal.with, + proposal.sessionId, + RtpEndUserState.DECLINED_OR_BUSY); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": no rtp session proposal found for " + + from + + " to deliver reject"); } } } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved out of order jingle message" + message); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": retrieved out of order jingle message" + + message); } - } - private RtpSessionProposal getRtpSessionProposal(final Account account, Jid from, String sessionId) { + private RtpSessionProposal getRtpSessionProposal( + final Account account, Jid from, String sessionId) { for (RtpSessionProposal rtpSessionProposal : rtpSessionProposals.keySet()) { - if (rtpSessionProposal.sessionId.equals(sessionId) && rtpSessionProposal.with.equals(from) && rtpSessionProposal.account.getJid().equals(account.getJid())) { + if (rtpSessionProposal.sessionId.equals(sessionId) + && rtpSessionProposal.with.equals(from) + && rtpSessionProposal.account.getJid().equals(account.getJid())) { return rtpSessionProposal; } } return null; } - private void writeLogMissedOutgoing(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) { - final Conversation conversation = mXmppConnectionService.findOrCreateConversation( - account, - with.asBareJid(), - false, - false - ); - final Message message = new Message( - conversation, - Message.STATUS_SEND, - Message.TYPE_RTP_SESSION, - sessionId - ); + private void writeLogMissedOutgoing( + final Account account, + Jid with, + final String sessionId, + String serverMsgId, + long timestamp) { + final Conversation conversation = + mXmppConnectionService.findOrCreateConversation( + account, with.asBareJid(), false, false); + final Message message = + new Message(conversation, Message.STATUS_SEND, Message.TYPE_RTP_SESSION, sessionId); message.setBody(new RtpSessionStatus(false, 0).toString()); message.setServerMsgId(serverMsgId); message.setTime(timestamp); writeMessage(message); } - private void writeLogMissedIncoming(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) { - final Conversation conversation = mXmppConnectionService.findOrCreateConversation( - account, - with.asBareJid(), - false, - false - ); - final Message message = new Message( - conversation, - Message.STATUS_RECEIVED, - Message.TYPE_RTP_SESSION, - sessionId - ); + private void writeLogMissedIncoming( + final Account account, + Jid with, + final String sessionId, + String serverMsgId, + long timestamp) { + final Conversation conversation = + mXmppConnectionService.findOrCreateConversation( + account, with.asBareJid(), false, false); + final Message message = + new Message( + conversation, Message.STATUS_RECEIVED, Message.TYPE_RTP_SESSION, sessionId); message.setBody(new RtpSessionStatus(false, 0).toString()); message.setServerMsgId(serverMsgId); message.setTime(timestamp); @@ -430,34 +542,41 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public void startJingleFileTransfer(final Message message) { - Preconditions.checkArgument(message.isFileOrImage(), "Message is not of type file or image"); + Preconditions.checkArgument( + message.isFileOrImage(), "Message is not of type file or image"); final Transferable old = message.getTransferable(); if (old != null) { old.cancel(); } final Account account = message.getConversation().getAccount(); final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message); - final JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id, account.getJid()); + final JingleFileTransferConnection connection = + new JingleFileTransferConnection(this, id, account.getJid()); mXmppConnectionService.markMessage(message, Message.STATUS_WAITING); this.connections.put(id, connection); connection.init(message); } public Optional getOngoingRtpConnection(final Contact contact) { - for (final Map.Entry entry : this.connections.entrySet()) { + for (final Map.Entry entry : + this.connections.entrySet()) { if (entry.getValue() instanceof JingleRtpConnection) { final AbstractJingleConnection.Id id = entry.getKey(); - if (id.account == contact.getAccount() && id.with.asBareJid().equals(contact.getJid().asBareJid())) { + if (id.account == contact.getAccount() + && id.with.asBareJid().equals(contact.getJid().asBareJid())) { return Optional.of(id); } } } synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { RtpSessionProposal proposal = entry.getKey(); - if (proposal.account == contact.getAccount() && contact.getJid().asBareJid().equals(proposal.with)) { + if (proposal.account == contact.getAccount() + && contact.getJid().asBareJid().equals(proposal.with)) { final DeviceDiscoveryState preexistingState = entry.getValue(); - if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { + if (preexistingState != null + && preexistingState != DeviceDiscoveryState.FAILED) { return Optional.of(proposal); } } @@ -473,7 +592,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { void finishConnectionOrThrow(final AbstractJingleConnection connection) { final AbstractJingleConnection.Id id = connection.getId(); if (this.connections.remove(id) == null) { - throw new IllegalStateException(String.format("Unable to finish connection with id=%s", id.toString())); + throw new IllegalStateException( + String.format("Unable to finish connection with id=%s", id.toString())); } } @@ -492,49 +612,70 @@ public class JingleConnectionManager extends AbstractConnectionManager { return firedUpdates; } - void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) { + void getPrimaryCandidate( + final Account account, + final boolean initiator, + final OnPrimaryCandidateFound listener) { if (Config.DISABLE_PROXY_LOOKUP) { listener.onPrimaryCandidateFound(false, null); return; } if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) { - final Jid proxy = account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS); + final Jid proxy = + account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS); if (proxy != null) { IqPacket iq = new IqPacket(IqPacket.TYPE.GET); iq.setTo(proxy); iq.query(Namespace.BYTE_STREAMS); - account.getXmppConnection().sendIqPacket(iq, new OnIqPacketReceived() { + account.getXmppConnection() + .sendIqPacket( + iq, + new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final Element streamhost = packet.query().findChild("streamhost", Namespace.BYTE_STREAMS); - final String host = streamhost == null ? null : streamhost.getAttribute("host"); - final String port = streamhost == null ? null : streamhost.getAttribute("port"); - if (host != null && port != null) { - try { - JingleCandidate candidate = new JingleCandidate(nextRandomId(), true); - candidate.setHost(host); - candidate.setPort(Integer.parseInt(port)); - candidate.setType(JingleCandidate.TYPE_PROXY); - candidate.setJid(proxy); - candidate.setPriority(655360 + (initiator ? 30 : 0)); - primaryCandidates.put(account.getJid().asBareJid(), candidate); - listener.onPrimaryCandidateFound(true, candidate); - } catch (final NumberFormatException e) { - listener.onPrimaryCandidateFound(false, null); - } - } else { - listener.onPrimaryCandidateFound(false, null); - } - } - }); + @Override + public void onIqPacketReceived( + Account account, IqPacket packet) { + final Element streamhost = + packet.query() + .findChild( + "streamhost", + Namespace.BYTE_STREAMS); + final String host = + streamhost == null + ? null + : streamhost.getAttribute("host"); + final String port = + streamhost == null + ? null + : streamhost.getAttribute("port"); + if (host != null && port != null) { + try { + JingleCandidate candidate = + new JingleCandidate(nextRandomId(), true); + candidate.setHost(host); + candidate.setPort(Integer.parseInt(port)); + candidate.setType(JingleCandidate.TYPE_PROXY); + candidate.setJid(proxy); + candidate.setPriority( + 655360 + (initiator ? 30 : 0)); + primaryCandidates.put( + account.getJid().asBareJid(), candidate); + listener.onPrimaryCandidateFound(true, candidate); + } catch (final NumberFormatException e) { + listener.onPrimaryCandidateFound(false, null); + } + } else { + listener.onPrimaryCandidateFound(false, null); + } + } + }); } else { listener.onPrimaryCandidateFound(false, null); } } else { - listener.onPrimaryCandidateFound(true, - this.primaryCandidates.get(account.getJid().asBareJid())); + listener.onPrimaryCandidateFound( + true, this.primaryCandidates.get(account.getJid().asBareJid())); } } @@ -556,64 +697,77 @@ public class JingleConnectionManager extends AbstractConnectionManager { private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) { final Account account = rtpSessionProposal.account; toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + rtpSessionProposal.with); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": retracting rtp session proposal with " + + rtpSessionProposal.with); this.rtpSessionProposals.remove(rtpSessionProposal); - final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); - writeLogMissedOutgoing(account, rtpSessionProposal.with, rtpSessionProposal.sessionId, null, System.currentTimeMillis()); + final MessagePacket messagePacket = + mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); + writeLogMissedOutgoing( + account, + rtpSessionProposal.with, + rtpSessionProposal.sessionId, + null, + System.currentTimeMillis()); mXmppConnectionService.sendMessagePacket(account, messagePacket); } - public String initializeRtpSession(final Account account, final Jid with, final Set media) { + public String initializeRtpSession( + final Account account, final Jid with, final Set media) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with); - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, account.getJid()); rtpConnection.setProposedMedia(media); this.connections.put(id, rtpConnection); rtpConnection.sendSessionInitiate(); return id.sessionId; } - public void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { + public void proposeJingleRtpSession( + final Account account, final Jid with, final Set media) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { RtpSessionProposal proposal = entry.getKey(); if (proposal.account == account && with.asBareJid().equals(proposal.with)) { final DeviceDiscoveryState preexistingState = entry.getValue(); - if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { + if (preexistingState != null + && preexistingState != DeviceDiscoveryState.FAILED) { final RtpEndUserState endUserState = preexistingState.toEndUserState(); toneManager.transition(endUserState, media); mXmppConnectionService.notifyJingleRtpConnectionUpdate( - account, - with, - proposal.sessionId, - endUserState - ); + account, with, proposal.sessionId, endUserState); return; } } } if (isBusy()) { if (hasMatchingRtpSession(account, with, media)) { - Log.d(Config.LOGTAG, "ignoring request to propose jingle session because the other party already created one for us"); + Log.d( + Config.LOGTAG, + "ignoring request to propose jingle session because the other party already created one for us"); return; } - throw new IllegalStateException("There is already a running RTP session. This should have been caught by the UI"); + throw new IllegalStateException( + "There is already a running RTP session. This should have been caught by the UI"); } - final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media); + final RtpSessionProposal proposal = + RtpSessionProposal.of(account, with.asBareJid(), media); this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( - account, - proposal.with, - proposal.sessionId, - RtpEndUserState.FINDING_DEVICE - ); - final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); + account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE); + final MessagePacket messagePacket = + mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); } } public boolean hasMatchingProposal(final Account account, final Jid with) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { final RtpSessionProposal proposal = entry.getKey(); if (proposal.account == account && with.asBareJid().equals(proposal.with)) { return true; @@ -642,10 +796,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (sid != null) { for (final AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleFileTransferConnection) { - final JingleFileTransferConnection fileTransfer = (JingleFileTransferConnection) connection; + final JingleFileTransferConnection fileTransfer = + (JingleFileTransferConnection) connection; final JingleTransport transport = fileTransfer.getTransport(); if (transport instanceof JingleInBandTransport) { - final JingleInBandTransport inBandTransport = (JingleInBandTransport) transport; + final JingleInBandTransport inBandTransport = + (JingleInBandTransport) transport; if (inBandTransport.matches(account, sid)) { inBandTransport.deliverPayload(packet, payload); } @@ -655,7 +811,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString()); - account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); + account.getXmppConnection() + .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); } public void notifyRebound(final Account account) { @@ -668,8 +825,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - public WeakReference findJingleRtpConnection(Account account, Jid with, String sessionId) { - final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); + public WeakReference findJingleRtpConnection( + Account account, Jid with, String sessionId) { + final AbstractJingleConnection.Id id = + AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection connection = connections.get(id); if (connection instanceof JingleRtpConnection) { return new WeakReference<>((JingleRtpConnection) connection); @@ -679,34 +838,53 @@ public class JingleConnectionManager extends AbstractConnectionManager { private void resendSessionProposals(final Account account) { synchronized (this.rtpSessionProposals) { - for (final Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (final Map.Entry entry : + this.rtpSessionProposals.entrySet()) { final RtpSessionProposal proposal = entry.getKey(); - if (entry.getValue() == DeviceDiscoveryState.SEARCHING && proposal.account == account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resending session proposal to " + proposal.with); - final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); + if (entry.getValue() == DeviceDiscoveryState.SEARCHING + && proposal.account == account) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resending session proposal to " + + proposal.with); + final MessagePacket messagePacket = + mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); } } } } - public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) { + public void updateProposedSessionDiscovered( + Account account, Jid from, String sessionId, final DeviceDiscoveryState target) { synchronized (this.rtpSessionProposals) { - final RtpSessionProposal sessionProposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); - final DeviceDiscoveryState currentState = sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal); + final RtpSessionProposal sessionProposal = + getRtpSessionProposal(account, from.asBareJid(), sessionId); + final DeviceDiscoveryState currentState = + sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal); if (currentState == null) { Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId); return; } if (currentState == DeviceDiscoveryState.DISCOVERED) { - Log.d(Config.LOGTAG, "session proposal already at discovered. not going to fall back"); + Log.d( + Config.LOGTAG, + "session proposal already at discovered. not going to fall back"); return; } this.rtpSessionProposals.put(sessionProposal, target); final RtpEndUserState endUserState = target.toEndUserState(); toneManager.transition(endUserState, sessionProposal.media); - mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, endUserState); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target); + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, sessionProposal.with, sessionProposal.sessionId, endUserState); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": flagging session " + + sessionId + + " as " + + target); } } @@ -731,7 +909,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public void failProceed(Account account, final Jid with, String sessionId) { - final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); + final AbstractJingleConnection.Id id = + AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection instanceof JingleRtpConnection) { ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(); @@ -742,13 +921,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (connections.containsValue(connection)) { return; } - final IllegalStateException e = new IllegalStateException("JingleConnection has not been registered with connection manager"); + final IllegalStateException e = + new IllegalStateException( + "JingleConnection has not been registered with connection manager"); Log.e(Config.LOGTAG, "ensureConnectionIsRegistered() failed. Going to throw", e); throw e; } - void setTerminalSessionState(AbstractJingleConnection.Id id, final RtpEndUserState state, final Set media) { - this.terminatedSessions.put(PersistableSessionId.of(id), new TerminatedRtpSession(state, media)); + void setTerminalSessionState( + AbstractJingleConnection.Id id, final RtpEndUserState state, final Set media) { + this.terminatedSessions.put( + PersistableSessionId.of(id), new TerminatedRtpSession(state, media)); } public TerminatedRtpSession getTerminalSessionState(final Jid with, final String sessionId) { @@ -773,8 +956,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PersistableSessionId that = (PersistableSessionId) o; - return Objects.equal(with, that.with) && - Objects.equal(sessionId, that.sessionId); + return Objects.equal(with, that.with) && Objects.equal(sessionId, that.sessionId); } @Override @@ -794,7 +976,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public enum DeviceDiscoveryState { - SEARCHING, SEARCHING_ACKNOWLEDGED, DISCOVERED, FAILED; + SEARCHING, + SEARCHING_ACKNOWLEDGED, + DISCOVERED, + FAILED; public RtpEndUserState toEndUserState() { switch (this) { @@ -835,9 +1020,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RtpSessionProposal proposal = (RtpSessionProposal) o; - return Objects.equal(account.getJid(), proposal.account.getJid()) && - Objects.equal(with, proposal.with) && - Objects.equal(sessionId, proposal.sessionId); + return Objects.equal(account.getJid(), proposal.account.getJid()) + && Objects.equal(with, proposal.with) + && Objects.equal(sessionId, proposal.sessionId); } @Override diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 9ed4d188f..d834d81a1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -33,6 +33,8 @@ import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -61,91 +63,103 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { +public class JingleRtpConnection extends AbstractJingleConnection + implements WebRTCWrapper.EventCallback { - public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( - State.PROCEED, - State.SESSION_INITIALIZED_PRE_APPROVED, - State.SESSION_ACCEPTED - ); + public static final List STATES_SHOWING_ONGOING_CALL = + Arrays.asList( + State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED); private static final long BUSY_TIME_OUT = 30; - private static final List TERMINATED = Arrays.asList( - State.ACCEPTED, - State.REJECTED, - State.REJECTED_RACED, - State.RETRACTED, - State.RETRACTED_RACED, - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - ); + private static final List TERMINATED = + Arrays.asList( + State.ACCEPTED, + State.REJECTED, + State.REJECTED_RACED, + State.RETRACTED, + State.RETRACTED_RACED, + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR); private static final Map> VALID_TRANSITIONS; static { - final ImmutableMap.Builder> transitionBuilder = new ImmutableMap.Builder<>(); - transitionBuilder.put(State.NULL, ImmutableList.of( + final ImmutableMap.Builder> transitionBuilder = + new ImmutableMap.Builder<>(); + transitionBuilder.put( + State.NULL, + ImmutableList.of( + State.PROPOSED, + State.SESSION_INITIALIZED, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); + transitionBuilder.put( State.PROPOSED, - State.SESSION_INITIALIZED, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - )); - transitionBuilder.put(State.PROPOSED, ImmutableList.of( - State.ACCEPTED, + ImmutableList.of( + State.ACCEPTED, + State.PROCEED, + State.REJECTED, + State.RETRACTED, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR, + State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection + // rebinds + )); + transitionBuilder.put( State.PROCEED, - State.REJECTED, - State.RETRACTED, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR, - State.TERMINATED_CONNECTIVITY_ERROR //only used when the xmpp connection rebinds - )); - transitionBuilder.put(State.PROCEED, ImmutableList.of( - State.REJECTED_RACED, - State.RETRACTED_RACED, + ImmutableList.of( + State.REJECTED_RACED, + State.RETRACTED_RACED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.TERMINATED_SUCCESS, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR, + State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error + // bounces of the proceed message + )); + transitionBuilder.put( + State.SESSION_INITIALIZED, + ImmutableList.of( + State.SESSION_ACCEPTED, + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors + // and IQ timeouts + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); + transitionBuilder.put( State.SESSION_INITIALIZED_PRE_APPROVED, - State.TERMINATED_SUCCESS, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR, - State.TERMINATED_CONNECTIVITY_ERROR //at this state used for error bounces of the proceed message - )); - transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of( + ImmutableList.of( + State.SESSION_ACCEPTED, + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors + // and IQ timeouts + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); + transitionBuilder.put( State.SESSION_ACCEPTED, - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, //at this state used for IQ errors and IQ timeouts - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - )); - transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of( - State.SESSION_ACCEPTED, - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, //at this state used for IQ errors and IQ timeouts - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - )); - transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of( - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - )); + ImmutableList.of( + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); VALID_TRANSITIONS = transitionBuilder.build(); } private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); - private final Queue> pendingIceCandidates = new LinkedList<>(); + private final Queue> + pendingIceCandidates = new LinkedList<>(); private final OmemoVerification omemoVerification = new OmemoVerification(); private final Message message; private State state = State.NULL; - private StateTransitionException stateTransitionException; private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; @@ -156,18 +170,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); - final Conversation conversation = jingleConnectionManager.getXmppConnectionService().findOrCreateConversation( - id.account, - id.with.asBareJid(), - false, - false - ); - this.message = new Message( - conversation, - isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED, - Message.TYPE_RTP_SESSION, - id.sessionId - ); + final Conversation conversation = + jingleConnectionManager + .getXmppConnectionService() + .findOrCreateConversation(id.account, id.with.asBareJid(), false, false); + this.message = + new Message( + conversation, + isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED, + Message.TYPE_RTP_SESSION, + id.sessionId); } private static State reasonToState(Reason reason) { @@ -208,7 +220,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web break; default: respondOk(jinglePacket); - Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction())); + Log.d( + Config.LOGTAG, + String.format( + "%s: received unhandled jingle action %s", + id.account.getJid().asBareJid(), jinglePacket.getAction())); break; } } @@ -222,8 +238,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } - if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { - //we might have already changed resources (full jid) at this point; so this might not even reach the other party + if (isInState( + State.SESSION_INITIALIZED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.SESSION_ACCEPTED)) { + // we might have already changed resources (full jid) at this point; so this might not + // even reach the other party sendSessionTerminate(Reason.CONNECTIVITY_ERROR); } else { transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); @@ -235,9 +255,21 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web respondOk(jinglePacket); final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); final State previous = this.state; - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + wrapper.reason + "(" + Strings.nullToEmpty(wrapper.text) + ") while in state " + previous); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received session terminate reason=" + + wrapper.reason + + "(" + + Strings.nullToEmpty(wrapper.text) + + ") while in state " + + previous); if (TERMINATED.contains(previous)) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring session terminate because already in " + previous); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring session terminate because already in " + + previous); return; } webRTCWrapper.close(); @@ -251,13 +283,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void receiveTransportInfo(final JinglePacket jinglePacket) { - //Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received - if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { + // Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to + // INITIALIZED only after transport-info has been received + if (isInState( + State.NULL, + State.PROCEED, + State.SESSION_INITIALIZED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.SESSION_ACCEPTED)) { final RtpContentMap contentMap; try { contentMap = RtpContentMap.of(jinglePacket); } catch (final IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": improperly formatted contents; ignoring", + e); respondOk(jinglePacket); return; } @@ -265,18 +307,27 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } else { if (isTerminated()) { respondOk(jinglePacket); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring out-of-order transport info; we where already terminated"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring out-of-order transport info; we where already terminated"); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received transport info while in state=" + + this.state); terminateWithOutOfOrder(jinglePacket); } } } - private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpContentMap contentMap) { - final Set> candidates = contentMap.contents.entrySet(); + private void receiveTransportInfo( + final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Set> candidates = + contentMap.contents.entrySet(); if (this.state == State.SESSION_ACCEPTED) { - //zero candidates + modified credentials are an ICE restart offer + // zero candidates + modified credentials are an ICE restart offer if (checkForIceRestart(jinglePacket, contentMap)) { return; } @@ -284,7 +335,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { processCandidates(candidates); } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); } } else { respondOk(jinglePacket); @@ -292,7 +346,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { + private boolean checkForIceRestart( + final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { final RtpContentMap existing = getRemoteContentMap(); final IceUdpTransportInfo.Credentials existingCredentials; final IceUdpTransportInfo.Credentials newCredentials; @@ -306,7 +361,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (existingCredentials.equals(newCredentials)) { return false; } - //TODO an alternative approach is to check if we already got an iq result to our ICE-restart + // TODO an alternative approach is to check if we already got an iq result to our + // ICE-restart // and if that's the case we are seeing an answer. // This might be more spec compliant but also more error prone potentially final boolean isOffer = rtpContentMap.emptyCandidates(); @@ -314,10 +370,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { if (isOffer) { Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials); - restartContentMap = existing.modifiedCredentials(newCredentials, IceUdpTransportInfo.Setup.ACTPASS); + restartContentMap = + existing.modifiedCredentials( + newCredentials, IceUdpTransportInfo.Setup.ACTPASS); } else { final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); - Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials + " peer_setup=" + setup); + Log.d( + Config.LOGTAG, + "received confirmation of ICE restart" + + newCredentials + + " peer_setup=" + + setup); // DTLS setup attribute needs to be rewritten to reflect current peer state // https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM restartContentMap = existing.modifiedCredentials(newCredentials, setup); @@ -333,7 +396,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web respondOk(jinglePacket); final Throwable rootCause = Throwables.getRootCause(exception); if (rootCause instanceof WebRTCWrapper.PeerConnectionNotInitialized) { - //If this happens a termination is already in progress + // If this happens a termination is already in progress Log.d(Config.LOGTAG, "ignoring PeerConnectionNotInitialized on ICE restart"); return true; } @@ -359,13 +422,22 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.peerDtlsSetup = setup; } - private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { + private boolean applyIceRestart( + final JinglePacket jinglePacket, + final RtpContentMap restartContentMap, + final boolean isOffer) + throws ExecutionException, InterruptedException { final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); - final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER; - org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(type, sessionDescription.toString()); + final org.webrtc.SessionDescription.Type type = + isOffer + ? org.webrtc.SessionDescription.Type.OFFER + : org.webrtc.SessionDescription.Type.ANSWER; + org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription(type, sessionDescription.toString()); if (isOffer && webRTCWrapper.getSignalingState() != PeerConnection.SignalingState.STABLE) { if (isInitiator()) { - //We ignore the offer and respond with tie-break. This will clause the responder not to apply the content map + // We ignore the offer and respond with tie-break. This will clause the responder + // not to apply the content map return false; } } @@ -375,7 +447,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription localSessionDescription = setLocalSessionDescription(); setLocalContentMap(RtpContentMap.of(localSessionDescription)); - //We need to respond OK before sending any candidates + // We need to respond OK before sending any candidates respondOk(jinglePacket); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } else { @@ -384,32 +456,40 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return true; } - private void processCandidates(final Set> contents) { + private void processCandidates( + final Set> contents) { for (final Map.Entry content : contents) { processCandidate(content); } } - private void processCandidate(final Map.Entry content) { + private void processCandidate( + final Map.Entry content) { final RtpContentMap rtpContentMap = getRemoteContentMap(); final List indices = toIdentificationTags(rtpContentMap); - final String sdpMid = content.getKey(); //aka content name + final String sdpMid = content.getKey(); // aka content name final IceUdpTransportInfo transport = content.getValue().transport; final IceUdpTransportInfo.Credentials credentials = transport.getCredentials(); - //TODO check that credentials remained the same + // TODO check that credentials remained the same for (final IceUdpTransportInfo.Candidate candidate : transport.getCandidates()) { final String sdp; try { sdp = candidate.toSdpAttribute(credentials.ufrag); } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring invalid ICE candidate " + + e.getMessage()); continue; } final int mLineIndex = indices.indexOf(sdpMid); if (mLineIndex < 0) { - Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices); + Log.w( + Config.LOGTAG, + "mLineIndex not found for " + sdpMid + ". available indices " + indices); } final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); @@ -423,14 +503,21 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private List toIdentificationTags(final RtpContentMap rtpContentMap) { final Group originalGroup = rtpContentMap.group; - final List identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags(); + final List identificationTags = + originalGroup == null + ? rtpContentMap.getNames() + : originalGroup.getIdentificationTags(); if (identificationTags.size() == 0) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); } return identificationTags; } - private ListenableFuture receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { + private ListenableFuture receiveRtpContentMap( + final JinglePacket jinglePacket, final boolean expectVerification) { final RtpContentMap receivedContentMap; try { receivedContentMap = RtpContentMap.of(jinglePacket); @@ -438,17 +525,26 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return Futures.immediateFailedFuture(e); } if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) { - final ListenableFuture> future = id.account.getAxolotlService().decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); - return Futures.transform(future, omemoVerifiedPayload -> { - //TODO test if an exception here triggers a correct abort - omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received verifiable DTLS fingerprint via " + omemoVerification); - return omemoVerifiedPayload.getPayload(); - }, MoreExecutors.directExecutor()); + final ListenableFuture> future = + id.account + .getAxolotlService() + .decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); + return Futures.transform( + future, + omemoVerifiedPayload -> { + // TODO test if an exception here triggers a correct abort + omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received verifiable DTLS fingerprint via " + + omemoVerification); + return omemoVerifiedPayload.getPayload(); + }, + MoreExecutors.directExecutor()); } else if (Config.REQUIRE_RTP_VERIFICATION || expectVerification) { return Futures.immediateFailedFuture( - new SecurityException("DTLS fingerprint was unexpectedly not verifiable") - ); + new SecurityException("DTLS fingerprint was unexpectedly not verifiable")); } else { return Futures.immediateFuture(receivedContentMap); } @@ -456,13 +552,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void receiveSessionInitiate(final JinglePacket jinglePacket) { if (isInitiator()) { - Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-initiate even though we were initiating", + id.account.getJid().asBareJid())); if (isTerminated()) { - Log.d(Config.LOGTAG, String.format( - "%s: got a reason to terminate with out-of-order. but already in state %s", - id.account.getJid().asBareJid(), - getState() - )); + Log.d( + Config.LOGTAG, + String.format( + "%s: got a reason to terminate with out-of-order. but already in state %s", + id.account.getJid().asBareJid(), getState())); respondWithOutOfOrder(jinglePacket); } else { terminateWithOutOfOrder(jinglePacket); @@ -470,43 +570,51 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } final ListenableFuture future = receiveRtpContentMap(jinglePacket, false); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(@Nullable RtpContentMap rtpContentMap) { - receiveSessionInitiate(jinglePacket, rtpContentMap); - } + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(@Nullable RtpContentMap rtpContentMap) { + receiveSessionInitiate(jinglePacket, rtpContentMap); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - respondOk(jinglePacket); - sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); - } - }, MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + respondOk(jinglePacket); + sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, + MoreExecutors.directExecutor()); } - private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) { + private void receiveSessionInitiate( + final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(true); } catch (final RuntimeException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); respondOk(jinglePacket); sendSessionTerminate(Reason.of(e), e.getMessage()); return; } - Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); + Log.d( + Config.LOGTAG, + "processing session-init with " + contentMap.contents.size() + " contents"); final State target; if (this.state == State.PROCEED) { Preconditions.checkState( proposedMedia != null && proposedMedia.size() > 0, - "proposed media must be set when processing pre-approved session-initiate" - ); + "proposed media must be set when processing pre-approved session-initiate"); if (!this.proposedMedia.equals(contentMap.getMedia())) { - sendSessionTerminate(Reason.SECURITY_ERROR, String.format( - "Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s", - this.proposedMedia, - contentMap.getMedia() - )); + sendSessionTerminate( + Reason.SECURITY_ERROR, + String.format( + "Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s", + this.proposedMedia, contentMap.getMedia())); return; } target = State.SESSION_INITIALIZED_PRE_APPROVED; @@ -517,67 +625,100 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web respondOk(jinglePacket); pendingIceCandidates.addAll(contentMap.contents.entrySet()); if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": automatically accepting session-initiate"); sendSessionAccept(); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received not pre-approved session-initiate. start ringing"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received not pre-approved session-initiate. start ringing"); startRinging(); } } else { - Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state)); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-initiate while in state %s", + id.account.getJid().asBareJid(), state)); terminateWithOutOfOrder(jinglePacket); } } private void receiveSessionAccept(final JinglePacket jinglePacket) { if (!isInitiator()) { - Log.d(Config.LOGTAG, String.format("%s: received session-accept even though we were responding", id.account.getJid().asBareJid())); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-accept even though we were responding", + id.account.getJid().asBareJid())); terminateWithOutOfOrder(jinglePacket); return; } - final ListenableFuture future = receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(@Nullable RtpContentMap rtpContentMap) { - receiveSessionAccept(jinglePacket, rtpContentMap); - } + final ListenableFuture future = + receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(@Nullable RtpContentMap rtpContentMap) { + receiveSessionAccept(jinglePacket, rtpContentMap); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - respondOk(jinglePacket); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", throwable); - webRTCWrapper.close(); - sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); - } - }, MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + respondOk(jinglePacket); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": improperly formatted contents in session-accept", + throwable); + webRTCWrapper.close(); + sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, + MoreExecutors.directExecutor()); } - private void receiveSessionAccept(final JinglePacket jinglePacket, final RtpContentMap contentMap) { + private void receiveSessionAccept( + final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); } catch (final RuntimeException e) { respondOk(jinglePacket); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": improperly formatted contents in session-accept", + e); webRTCWrapper.close(); sendSessionTerminate(Reason.of(e), e.getMessage()); return; } final Set initiatorMedia = this.initiatorRtpContentMap.getMedia(); if (!initiatorMedia.equals(contentMap.getMedia())) { - sendSessionTerminate(Reason.SECURITY_ERROR, String.format( - "Your session-included included media %s but our session-initiate was %s", - this.proposedMedia, - contentMap.getMedia() - )); + sendSessionTerminate( + Reason.SECURITY_ERROR, + String.format( + "Your session-included included media %s but our session-initiate was %s", + this.proposedMedia, contentMap.getMedia())); return; } - Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents"); + Log.d( + Config.LOGTAG, + "processing session-accept with " + contentMap.contents.size() + " contents"); if (transition(State.SESSION_ACCEPTED)) { respondOk(jinglePacket); receiveSessionAccept(contentMap); } else { - Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state)); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-accept while in state %s", + id.account.getJid().asBareJid(), state)); respondOk(jinglePacket); } } @@ -589,21 +730,29 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { sessionDescription = SessionDescription.of(contentMap); } catch (final IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-accept to SDP", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable convert offer from session-accept to SDP", + e); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; } - final org.webrtc.SessionDescription answer = new org.webrtc.SessionDescription( - org.webrtc.SessionDescription.Type.ANSWER, - sessionDescription.toString() - ); + final org.webrtc.SessionDescription answer = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.ANSWER, sessionDescription.toString()); try { this.webRTCWrapper.setRemoteDescription(answer).get(); } catch (final Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e)); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to set remote description after receiving session-accept", + Throwables.getRootCause(e)); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage()); + sendSessionTerminate( + Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage()); return; } processCandidates(contentMap.contents.entrySet()); @@ -618,7 +767,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { offer = SessionDescription.of(rtpContentMap); } catch (final IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-initiate to SDP", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable convert offer from session-initiate to SDP", + e); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; @@ -630,9 +783,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web discoverIceServers(iceServers -> sendSessionAccept(media, offer, iceServers)); } - private synchronized void sendSessionAccept(final Set media, final SessionDescription offer, final List iceServers) { + private synchronized void sendSessionAccept( + final Set media, + final SessionDescription offer, + final List iceServers) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": ICE servers got discovered when session was already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } try { @@ -643,14 +802,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionTerminate(Reason.FAILED_APPLICATION); return; } - final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription( - org.webrtc.SessionDescription.Type.OFFER, - offer.toString() - ); + final org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.OFFER, offer.toString()); try { this.webRTCWrapper.setRemoteDescription(sdp).get(); addIceCandidatesFromBlackLog(); - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); + org.webrtc.SessionDescription webRTCSessionDescription = + this.webRTCWrapper.setLocalDescription().get(); prepareSessionAccept(webRTCSessionDescription); } catch (final Exception e) { failureToAcceptSession(e); @@ -671,18 +830,24 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Map.Entry foo; while ((foo = this.pendingIceCandidates.poll()) != null) { processCandidate(foo); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidate from back log"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": added candidate from back log"); } } - private void prepareSessionAccept(final org.webrtc.SessionDescription webRTCSessionDescription) { - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + private void prepareSessionAccept( + final org.webrtc.SessionDescription webRTCSessionDescription) { + final SessionDescription sessionDescription = + SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); this.responderRtpContentMap = respondingRtpContentMap; storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - final ListenableFuture outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); - Futures.addCallback(outgoingContentMapFuture, + final ListenableFuture outgoingContentMapFuture = + prepareOutgoingContentMap(respondingRtpContentMap); + Futures.addCallback( + outgoingContentMapFuture, new FutureCallback() { @Override public void onSuccess(final RtpContentMap outgoingContentMap) { @@ -694,35 +859,56 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web failureToAcceptSession(throwable); } }, - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } private void sendSessionAccept(final RtpContentMap rtpContentMap) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": preparing session accept was too slow. already terminated. nothing to do."); return; } transitionOrThrow(State.SESSION_ACCEPTED); - final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); + final JinglePacket sessionAccept = + rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); send(sessionAccept); } - private ListenableFuture prepareOutgoingContentMap(final RtpContentMap rtpContentMap) { + private ListenableFuture prepareOutgoingContentMap( + final RtpContentMap rtpContentMap) { if (this.omemoVerification.hasDeviceId()) { - ListenableFuture> verifiedPayloadFuture = id.account.getAxolotlService() - .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); - return Futures.transform(verifiedPayloadFuture, verifiedPayload -> { - omemoVerification.setOrEnsureEqual(verifiedPayload); - return verifiedPayload.getPayload(); - }, MoreExecutors.directExecutor()); + ListenableFuture> + verifiedPayloadFuture = + id.account + .getAxolotlService() + .encrypt( + rtpContentMap, + id.with, + omemoVerification.getDeviceId()); + return Futures.transform( + verifiedPayloadFuture, + verifiedPayload -> { + omemoVerification.setOrEnsureEqual(verifiedPayload); + return verifiedPayload.getPayload(); + }, + MoreExecutors.directExecutor()); } else { return Futures.immediateFuture(rtpContentMap); } } - synchronized void deliveryMessage(final Jid from, final Element message, final String serverMessageId, final long timestamp) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); + synchronized void deliveryMessage( + final Jid from, + final Element message, + final String serverMessageId, + final long timestamp) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": delivered message to JingleRtpConnection " + + message); switch (message.getName()) { case "propose": receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp); @@ -745,47 +931,73 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } void deliverFailedProceed() { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receive message error for proceed message"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": receive message error for proceed message"); if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) { webRTCWrapper.close(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into connectivity error"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": transitioned into connectivity error"); this.finish(); } } private void receiveAccept(final Jid from, final String serverMsgId, final long timestamp) { - final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + final boolean originatedFromMyself = + from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { if (transition(State.ACCEPTED)) { if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } this.message.setTime(timestamp); - this.message.setCarbon(true); //indicate that call was accepted on other device + this.message.setCarbon(true); // indicate that call was accepted on other device this.writeLogMessageSuccess(0); - this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + this.xmppConnectionService + .getNotificationService() + .cancelIncomingCallNotification(); this.finish(); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to transition to accept because already in state=" + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to transition to accept because already in state=" + + this.state); } } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring 'accept' from " + from); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": ignoring 'accept' from " + from); } } private void receiveReject(final Jid from, final String serverMsgId, final long timestamp) { - final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); - //reject from another one of my clients + final boolean originatedFromMyself = + from.asBareJid().equals(id.account.getJid().asBareJid()); + // reject from another one of my clients if (originatedFromMyself) { receiveRejectFromMyself(serverMsgId, timestamp); } else if (isInitiator()) { if (from.equals(id.with)) { receiveRejectFromResponder(); } else { - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from " + from + " for session with " + id.with); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring reject from " + + from + + " for session with " + + id.with); } } else { - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from " + from + " for session with " + id.with); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring reject from " + + from + + " for session with " + + id.with); } } @@ -797,54 +1009,94 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.message.setServerMsgId(serverMsgId); } this.message.setTime(timestamp); - this.message.setCarbon(true); //indicate that call was rejected on other device + this.message.setCarbon(true); // indicate that call was rejected on other device writeLogMessageMissed(); } else { - Log.d(Config.LOGTAG, "not able to transition into REJECTED because already in " + this.state); + Log.d( + Config.LOGTAG, + "not able to transition into REJECTED because already in " + this.state); } } private void receiveRejectFromResponder() { if (isInState(State.PROCEED)) { - Log.d(Config.LOGTAG, id.account.getJid() + ": received reject while still in proceed. callee reconsidered"); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": received reject while still in proceed. callee reconsidered"); closeTransitionLogFinish(State.REJECTED_RACED); return; } if (isInState(State.SESSION_INITIALIZED_PRE_APPROVED)) { - Log.d(Config.LOGTAG, id.account.getJid() + ": received reject while in SESSION_INITIATED_PRE_APPROVED. callee reconsidered before receiving session-init"); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": received reject while in SESSION_INITIATED_PRE_APPROVED. callee reconsidered before receiving session-init"); closeTransitionLogFinish(State.TERMINATED_DECLINED_OR_BUSY); return; } - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from responder because already in state " + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring reject from responder because already in state " + + this.state); } - private void receivePropose(final Jid from, final Propose propose, final String serverMsgId, final long timestamp) { - final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + private void receivePropose( + final Jid from, final Propose propose, final String serverMsgId, final long timestamp) { + final boolean originatedFromMyself = + from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from myself. ignoring"); - } else if (transition(State.PROPOSED, () -> { - final Collection descriptions = Collections2.transform( - Collections2.filter(propose.getDescriptions(), d -> d instanceof RtpDescription), - input -> (RtpDescription) input - ); - final Collection media = Collections2.transform(descriptions, RtpDescription::getMedia); - Preconditions.checkState(!media.contains(Media.UNKNOWN), "RTP descriptions contain unknown media"); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session proposal from " + from + " for " + media); - this.proposedMedia = Sets.newHashSet(media); - })) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": saw proposal from myself. ignoring"); + } else if (transition( + State.PROPOSED, + () -> { + final Collection descriptions = + Collections2.transform( + Collections2.filter( + propose.getDescriptions(), + d -> d instanceof RtpDescription), + input -> (RtpDescription) input); + final Collection media = + Collections2.transform(descriptions, RtpDescription::getMedia); + Preconditions.checkState( + !media.contains(Media.UNKNOWN), + "RTP descriptions contain unknown media"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received session proposal from " + + from + + " for " + + media); + this.proposedMedia = Sets.newHashSet(media); + })) { if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } this.message.setTime(timestamp); startRinging(); } else { - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring session proposal because already in " + + state); } } private void startRinging() { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received call from " + id.with + ". start ringing"); - ringingTimeoutFuture = jingleConnectionManager.schedule(this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received call from " + + id.with + + ". start ringing"); + ringingTimeoutFuture = + jingleConnectionManager.schedule( + this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS); xmppConnectionService.getNotificationService().startRinging(id, getMedia()); } @@ -869,8 +1121,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void receiveProceed(final Jid from, final Proceed proceed, final String serverMsgId, final long timestamp) { - final Set media = Preconditions.checkNotNull(this.proposedMedia, "Proposed media has to be set before handling proceed"); + private void receiveProceed( + final Jid from, final Proceed proceed, final String serverMsgId, final long timestamp) { + final Set media = + Preconditions.checkNotNull( + this.proposedMedia, "Proposed media has to be set before handling proceed"); Preconditions.checkState(media.size() > 0, "Proposed media should not be empty"); if (from.equals(id.with)) { if (isInitiator()) { @@ -884,34 +1139,64 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.omemoVerification.setDeviceId(remoteDeviceId); } else { if (remoteDeviceId != null) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote party signaled support for OMEMO verification but we have OMEMO disabled"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": remote party signaled support for OMEMO verification but we have OMEMO disabled"); } this.omemoVerification.setDeviceId(null); } this.sendSessionInitiate(media, State.SESSION_INITIALIZED_PRE_APPROVED); } else { - Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state)); + Log.d( + Config.LOGTAG, + String.format( + "%s: ignoring proceed because already in %s", + id.account.getJid().asBareJid(), this.state)); } } else { - Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because we were not initializing", id.account.getJid().asBareJid())); + Log.d( + Config.LOGTAG, + String.format( + "%s: ignoring proceed because we were not initializing", + id.account.getJid().asBareJid())); } } else if (from.asBareJid().equals(id.account.getJid().asBareJid())) { if (transition(State.ACCEPTED)) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": moved session with " + id.with + " into state accepted after received carbon copied procced"); - this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": moved session with " + + id.with + + " into state accepted after received carbon copied procced"); + this.xmppConnectionService + .getNotificationService() + .cancelIncomingCallNotification(); this.finish(); } } else { - Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with)); + Log.d( + Config.LOGTAG, + String.format( + "%s: ignoring proceed from %s. was expected from %s", + id.account.getJid().asBareJid(), from, id.with)); } } private void receiveRetract(final Jid from, final String serverMsgId, final long timestamp) { if (from.equals(id.with)) { - final State target = this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED; + final State target = + this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED; if (transition(target)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted (serverMsgId=" + serverMsgId + ")"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": session with " + + id.with + + " has been retracted (serverMsgId=" + + serverMsgId + + ")"); if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } @@ -925,8 +1210,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state); } } else { - //TODO parse retract from self - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received retract from " + from + ". expected retract from" + id.with + ". ignoring"); + // TODO parse retract from self + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received retract from " + + from + + ". expected retract from" + + id.with + + ". ignoring"); } } @@ -939,9 +1231,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web discoverIceServers(iceServers -> sendSessionInitiate(media, targetState, iceServers)); } - private synchronized void sendSessionInitiate(final Set media, final State targetState, final List iceServers) { + private synchronized void sendSessionInitiate( + final Set media, + final State targetState, + final List iceServers) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": ICE servers got discovered when session was already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } try { @@ -953,10 +1251,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } try { - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); + org.webrtc.SessionDescription webRTCSessionDescription = + this.webRTCWrapper.setLocalDescription().get(); prepareSessionInitiate(webRTCSessionDescription, targetState); } catch (final Exception e) { - //TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions + // TODO sending the error text is worthwhile as well. Especially for FailureToSet + // exceptions failureToInitiateSession(e, targetState); } } @@ -965,7 +1265,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (isTerminated()) { return; } - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", Throwables.getRootCause(throwable)); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", + Throwables.getRootCause(throwable)); webRTCWrapper.close(); final Reason reason = Reason.ofThrowable(throwable); if (isInState(targetState)) { @@ -976,49 +1279,71 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void sendRetract(final Reason reason) { - //TODO embed reason into retract + // TODO embed reason into retract sendJingleMessage("retract", id.with.asBareJid()); transitionOrThrow(reasonToState(reason)); this.finish(); } - private void prepareSessionInitiate(final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + private void prepareSessionInitiate( + final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { + final SessionDescription sessionDescription = + SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); this.initiatorRtpContentMap = rtpContentMap; this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); - Futures.addCallback(outgoingContentMapFuture, new FutureCallback() { - @Override - public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionInitiate(outgoingContentMap, targetState); - } + final ListenableFuture outgoingContentMapFuture = + encryptSessionInitiate(rtpContentMap); + Futures.addCallback( + outgoingContentMapFuture, + new FutureCallback() { + @Override + public void onSuccess(final RtpContentMap outgoingContentMap) { + sendSessionInitiate(outgoingContentMap, targetState); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - failureToInitiateSession(throwable, targetState); - } - }, MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + failureToInitiateSession(throwable, targetState); + } + }, + MoreExecutors.directExecutor()); } private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": preparing session was too slow. already terminated. nothing to do."); return; } this.transitionOrThrow(targetState); - final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); + final JinglePacket sessionInitiate = + rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); send(sessionInitiate); } - private ListenableFuture encryptSessionInitiate(final RtpContentMap rtpContentMap) { + private ListenableFuture encryptSessionInitiate( + final RtpContentMap rtpContentMap) { if (this.omemoVerification.hasDeviceId()) { - final ListenableFuture> verifiedPayloadFuture = id.account.getAxolotlService() - .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); - final ListenableFuture future = Futures.transform(verifiedPayloadFuture, verifiedPayload -> { - omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint()); - return verifiedPayload.getPayload(); - }, MoreExecutors.directExecutor()); + final ListenableFuture> + verifiedPayloadFuture = + id.account + .getAxolotlService() + .encrypt( + rtpContentMap, + id.with, + omemoVerification.getDeviceId()); + final ListenableFuture future = + Futures.transform( + verifiedPayloadFuture, + verifiedPayload -> { + omemoVerification.setSessionFingerprint( + verifiedPayload.getFingerprint()); + return verifiedPayload.getPayload(); + }, + MoreExecutors.directExecutor()); if (Config.REQUIRE_RTP_VERIFICATION) { return future; } @@ -1026,11 +1351,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web future, CryptoFailedException.class, e -> { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", + e); return rtpContentMap; }, - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } else { return Futures.immediateFuture(rtpContentMap); } @@ -1047,23 +1375,31 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (previous != State.NULL) { writeLogMessage(target); } - final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + final JinglePacket jinglePacket = + new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); Log.d(Config.LOGTAG, jinglePacket.toString()); send(jinglePacket); finish(); } - private void sendTransportInfo(final String contentName, IceUdpTransportInfo.Candidate candidate) { + private void sendTransportInfo( + final String contentName, IceUdpTransportInfo.Candidate candidate) { final RtpContentMap transportInfo; try { - final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final RtpContentMap rtpContentMap = + isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; transportInfo = rtpContentMap.transportInfo(contentName, candidate); } catch (final Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to prepare transport-info from candidate for content=" + + contentName); return; } - final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + final JinglePacket jinglePacket = + transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); send(jinglePacket); } @@ -1085,19 +1421,28 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void handleIqErrorResponse(final IqPacket response) { Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); final String errorCondition = response.getErrorCondition(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received IQ-error from " + + response.getFrom() + + " in RTP session. " + + errorCondition); if (isTerminated()) { - Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + Log.i( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring error because session was already terminated"); return; } this.webRTCWrapper.close(); final State target; if (Arrays.asList( - "service-unavailable", - "recipient-unavailable", - "remote-server-not-found", - "remote-server-timeout" - ).contains(errorCondition)) { + "service-unavailable", + "recipient-unavailable", + "remote-server-not-found", + "remote-server-timeout") + .contains(errorCondition)) { target = State.TERMINATED_CONNECTIVITY_ERROR; } else { target = State.TERMINATED_APPLICATION_FAILURE; @@ -1108,9 +1453,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void handleIqTimeoutResponse(final IqPacket response) { Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received IQ timeout in RTP session with " + + id.with + + ". terminating with connectivity error"); if (isTerminated()) { - Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + Log.i( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring error because session was already terminated"); return; } this.webRTCWrapper.close(); @@ -1119,7 +1472,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminating session with out-of-order"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": terminating session with out-of-order"); this.webRTCWrapper.close(); transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); respondWithOutOfOrder(jinglePacket); @@ -1134,19 +1489,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); } - void respondWithJingleError(final IqPacket original, String jingleCondition, String condition, String conditionType) { - jingleConnectionManager.respondWithJingleError(id.account, original, jingleCondition, condition, conditionType); + void respondWithJingleError( + final IqPacket original, + String jingleCondition, + String condition, + String conditionType) { + jingleConnectionManager.respondWithJingleError( + id.account, original, jingleCondition, condition, conditionType); } private void respondOk(final JinglePacket jinglePacket) { - xmppConnectionService.sendIqPacket(id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null); - } - - public void throwStateTransitionException() { - final StateTransitionException exception = this.stateTransitionException; - if (exception != null) { - throw new IllegalStateException(String.format("Transition to %s did not call finish", exception.state), exception); - } + xmppConnectionService.sendIqPacket( + id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null); } public RtpEndUserState getEndUserState() { @@ -1193,23 +1547,25 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return RtpEndUserState.RETRACTED; } case TERMINATED_CONNECTIVITY_ERROR: - return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; + return zeroDuration() + ? RtpEndUserState.CONNECTIVITY_ERROR + : RtpEndUserState.CONNECTIVITY_LOST_ERROR; case TERMINATED_APPLICATION_FAILURE: return RtpEndUserState.APPLICATION_ERROR; case TERMINATED_SECURITY_ERROR: return RtpEndUserState.SECURITY_ERROR; } - throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); + throw new IllegalStateException( + String.format("%s has no equivalent EndUserState", this.state)); } - private RtpEndUserState getPeerConnectionStateAsEndUserState() { final PeerConnection.PeerConnectionState state; try { state = webRTCWrapper.getState(); } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - //We usually close the WebRTCWrapper *before* transitioning so we might still - //be in SESSION_ACCEPTED even though the peerConnection has been torn down + // We usually close the WebRTCWrapper *before* transitioning so we might still + // be in SESSION_ACCEPTED even though the peerConnection has been torn down return RtpEndUserState.ENDING_CALL; } switch (state) { @@ -1221,7 +1577,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case CLOSED: return RtpEndUserState.ENDING_CALL; default: - return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.RECONNECTING; + return zeroDuration() + ? RtpEndUserState.CONNECTIVITY_ERROR + : RtpEndUserState.RECONNECTING; } } @@ -1230,35 +1588,32 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (current == State.NULL) { if (isInitiator()) { return Preconditions.checkNotNull( - this.proposedMedia, - "RTP connection has not been initialized properly" - ); + this.proposedMedia, "RTP connection has not been initialized properly"); } throw new IllegalStateException("RTP connection has not been initialized yet"); } if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) { return Preconditions.checkNotNull( - this.proposedMedia, - "RTP connection has not been initialized properly" - ); + this.proposedMedia, "RTP connection has not been initialized properly"); } final RtpContentMap initiatorContentMap = initiatorRtpContentMap; if (initiatorContentMap != null) { return initiatorContentMap.getMedia(); } else if (isTerminated()) { - return Collections.emptySet(); //we might fail before we ever got a chance to set media + return Collections.emptySet(); // we might fail before we ever got a chance to set media } else { - return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); + return Preconditions.checkNotNull( + this.proposedMedia, "RTP connection has not been initialized properly"); } } - public boolean isVerified() { final String fingerprint = this.omemoVerification.getFingerprint(); if (fingerprint == null) { return false; } - final FingerprintStatus status = id.account.getAxolotlService().getFingerprintTrust(fingerprint); + final FingerprintStatus status = + id.account.getAxolotlService().getFingerprintTrust(fingerprint); return status != null && status.isVerified(); } @@ -1273,18 +1628,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web acceptCallFromSessionInitialized(); break; case ACCEPTED: - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": the call has already been accepted with another client. UI was just lagging behind"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": the call has already been accepted with another client. UI was just lagging behind"); break; case PROCEED: case SESSION_ACCEPTED: - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": the call has already been accepted. user probably double tapped the UI"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": the call has already been accepted. user probably double tapped the UI"); break; default: throw new IllegalStateException("Can not accept call from " + this.state); } } - public void notifyPhoneCall() { Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections"); if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) { @@ -1296,7 +1656,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public synchronized void rejectCall() { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": received rejectCall() when session has already been terminated. nothing to do"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received rejectCall() when session has already been terminated. nothing to do"); return; } switch (this.state) { @@ -1313,7 +1676,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public synchronized void endCall() { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": received endCall() when session has already been terminated. nothing to do"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received endCall() when session has already been terminated. nothing to do"); return; } if (isInState(State.PROPOSED) && !isInitiator()) { @@ -1328,7 +1694,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } return; } - if (isInitiator() && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) { + if (isInitiator() + && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) { this.webRTCWrapper.close(); sendSessionTerminate(Reason.CANCEL); return; @@ -1342,11 +1709,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionTerminate(Reason.SUCCESS); return; } - if (isInState(State.TERMINATED_APPLICATION_FAILURE, State.TERMINATED_CONNECTIVITY_ERROR, State.TERMINATED_DECLINED_OR_BUSY)) { - Log.d(Config.LOGTAG, "ignoring request to end call because already in state " + this.state); + if (isInState( + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_DECLINED_OR_BUSY)) { + Log.d( + Config.LOGTAG, + "ignoring request to end call because already in state " + this.state); return; } - throw new IllegalStateException("called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator()); + throw new IllegalStateException( + "called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator()); } private void retractFromProceed() { @@ -1362,7 +1735,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web finish(); } - private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { + private void setupWebRTC( + final Set media, final List iceServers) + throws WebRTCWrapper.InitializationException { this.jingleConnectionManager.ensureConnectionIsRegistered(this); final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference; if (media.contains(Media.VIDEO)) { @@ -1406,14 +1781,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendJingleMessage(final String action, final Jid to) { final MessagePacket messagePacket = new MessagePacket(); - messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + messagePacket.setType(MessagePacket.TYPE_CHAT); // we want to carbon copy those messagePacket.setTo(to); - final Element intent = messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); + final Element intent = + messagePacket + .addChild(action, Namespace.JINGLE_MESSAGE) + .setAttribute("id", id.sessionId); if ("proceed".equals(action)) { messagePacket.setId(JINGLE_MESSAGE_PROCEED_ID_PREFIX + id.sessionId); if (isOmemoEnabled()) { final int deviceId = id.account.getAxolotlService().getOwnDeviceId(); - final Element device = intent.addChild("device", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); + final Element device = + intent.addChild("device", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); device.setAttribute("id", deviceId); } } @@ -1424,7 +1803,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private boolean isOmemoEnabled() { final Conversational conversational = message.getConversation(); if (conversational instanceof Conversation) { - return ((Conversation) conversational).getNextEncryption() == Message.ENCRYPTION_AXOLOTL; + return ((Conversation) conversational).getNextEncryption() + == Message.ENCRYPTION_AXOLOTL; } return false; } @@ -1446,7 +1826,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final Collection validTransitions = VALID_TRANSITIONS.get(this.state); if (validTransitions != null && validTransitions.contains(target)) { this.state = target; - this.stateTransitionException = new StateTransitionException(target); if (runnable != null) { runnable.run(); } @@ -1461,26 +1840,31 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web void transitionOrThrow(final State target) { if (!transition(target)) { - throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target)); + throw new IllegalStateException( + String.format("Unable to transition from %s to %s", this.state, target)); } } @Override public void onIceCandidate(final IceCandidate iceCandidate) { - final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final RtpContentMap rtpContentMap = + isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; final String ufrag = rtpContentMap.getCredentials().ufrag; - final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag); + final IceUdpTransportInfo.Candidate candidate = + IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag); if (candidate == null) { - Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate.toString()); + Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate); return; } - Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); + Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate); sendTransportInfo(iceCandidate.sdpMid, candidate); } @Override public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); this.stateHistory.add(newState); if (newState == PeerConnection.PeerConnectionState.CONNECTED) { this.sessionDuration.start(); @@ -1490,12 +1874,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web updateOngoingCallNotification(); } - final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); + final boolean neverConnected = + !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); if (newState == PeerConnection.PeerConnectionState.FAILED) { if (neverConnected) { if (isTerminated()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": not sending session-terminate after connectivity error because session is already in state " + + this.state); return; } webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection); @@ -1513,7 +1902,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void initiateIceRestart() { - //TODO discover new TURN/STUN credentials + // TODO discover new TURN/STUN credentials this.stateHistory.clear(); this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription sessionDescription; @@ -1527,28 +1916,32 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap transportInfo = rtpContentMap.transportInfo(); - final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + final JinglePacket jinglePacket = + transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket); jinglePacket.setTo(id.with); - xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, "received success to our ice restart"); - setLocalContentMap(rtpContentMap); - webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - return; - } - if (response.getType() == IqPacket.TYPE.ERROR) { - final Element error = response.findChild("error"); - if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { - Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); - return; - } - handleIqErrorResponse(response); - } - if (response.getType() == IqPacket.TYPE.TIMEOUT) { - handleIqTimeoutResponse(response); - } - }); + xmppConnectionService.sendIqPacket( + id.account, + jinglePacket, + (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, "received success to our ice restart"); + setLocalContentMap(rtpContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + return; + } + if (response.getType() == IqPacket.TYPE.ERROR) { + final Element error = response.findChild("error"); + if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { + Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); + return; + } + handleIqErrorResponse(response); + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + }); } private void setLocalContentMap(final RtpContentMap rtpContentMap) { @@ -1567,8 +1960,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException { - final org.webrtc.SessionDescription sessionDescription = this.webRTCWrapper.setLocalDescription().get(); + private SessionDescription setLocalSessionDescription() + throws ExecutionException, InterruptedException { + final org.webrtc.SessionDescription sessionDescription = + this.webRTCWrapper.setLocalDescription().get(); return SessionDescription.parse(sessionDescription.description); } @@ -1576,7 +1971,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.webRTCWrapper.close(); synchronized (this) { if (isTerminated()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": no need to send session-terminate after failed connection. Other party already did"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": no need to send session-terminate after failed connection. Other party already did"); return; } sendSessionTerminate(Reason.CONNECTIVITY_ERROR); @@ -1624,14 +2022,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } @Override - public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { - xmppConnectionService.notifyJingleRtpConnectionUpdate(selectedAudioDevice, availableAudioDevices); + public void onAudioDeviceChanged( + AppRTCAudioManager.AudioDevice selectedAudioDevice, + Set availableAudioDevices) { + xmppConnectionService.notifyJingleRtpConnectionUpdate( + selectedAudioDevice, availableAudioDevices); } private void updateEndUserState() { final RtpEndUserState endUserState = getEndUserState(); jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia()); - xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState); + xmppConnectionService.notifyJingleRtpConnectionUpdate( + id.account, id.with, id.sessionId, endUserState); } private void updateOngoingCallNotification() { @@ -1639,7 +2041,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (STATES_SHOWING_ONGOING_CALL.contains(state)) { final boolean reconnecting; if (state == State.SESSION_ACCEPTED) { - reconnecting = getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING; + reconnecting = + getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING; } else { reconnecting = false; } @@ -1654,58 +2057,102 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final IqPacket request = new IqPacket(IqPacket.TYPE.GET); request.setTo(id.account.getDomain()); request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); - xmppConnectionService.sendIqPacket(id.account, request, (account, response) -> { - ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element services = response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); - final List children = services == null ? Collections.emptyList() : services.getChildren(); - for (final Element child : children) { - if ("service".equals(child.getName())) { - final String type = child.getAttribute("type"); - final String host = child.getAttribute("host"); - final String sport = child.getAttribute("port"); - final Integer port = sport == null ? null : Ints.tryParse(sport); - final String transport = child.getAttribute("transport"); - final String username = child.getAttribute("username"); - final String password = child.getAttribute("password"); - if (Strings.isNullOrEmpty(host) || port == null) { - continue; - } - if (port < 0 || port > 65535) { - continue; - } - if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type) && Arrays.asList("udp", "tcp").contains(transport)) { - if (Arrays.asList("stuns", "turns").contains(type) && "udp".equals(transport)) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": skipping invalid combination of udp/tls in external services"); - continue; + xmppConnectionService.sendIqPacket( + id.account, + request, + (account, response) -> { + ImmutableList.Builder listBuilder = + new ImmutableList.Builder<>(); + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element services = + response.findChild( + "services", Namespace.EXTERNAL_SERVICE_DISCOVERY); + final List children = + services == null + ? Collections.emptyList() + : services.getChildren(); + for (final Element child : children) { + if ("service".equals(child.getName())) { + final String type = child.getAttribute("type"); + final String host = child.getAttribute("host"); + final String sport = child.getAttribute("port"); + final Integer port = + sport == null ? null : Ints.tryParse(sport); + final String transport = child.getAttribute("transport"); + final String username = child.getAttribute("username"); + final String password = child.getAttribute("password"); + if (Strings.isNullOrEmpty(host) || port == null) { + continue; + } + if (port < 0 || port > 65535) { + continue; + } + if (Arrays.asList("stun", "stuns", "turn", "turns") + .contains(type) + && Arrays.asList("udp", "tcp").contains(transport)) { + if (Arrays.asList("stuns", "turns").contains(type) + && "udp".equals(transport)) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": skipping invalid combination of udp/tls in external services"); + continue; + } + final PeerConnection.IceServer.Builder iceServerBuilder = + PeerConnection.IceServer.builder( + String.format( + "%s:%s:%s?transport=%s", + type, + IP.wrapIPv6(host), + port, + transport)); + iceServerBuilder.setTlsCertPolicy( + PeerConnection.TlsCertPolicy + .TLS_CERT_POLICY_INSECURE_NO_CHECK); + if (username != null && password != null) { + iceServerBuilder.setUsername(username); + iceServerBuilder.setPassword(password); + } else if (Arrays.asList("turn", "turns").contains(type)) { + // The WebRTC spec requires throwing an + // InvalidAccessError when username (from libwebrtc + // source coder) + // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": skipping " + + type + + "/" + + transport + + " without username and password"); + continue; + } + final PeerConnection.IceServer iceServer = + iceServerBuilder.createIceServer(); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": discovered ICE Server: " + + iceServer); + listBuilder.add(iceServer); + } } - final PeerConnection.IceServer.Builder iceServerBuilder = PeerConnection.IceServer - .builder(String.format("%s:%s:%s?transport=%s", type, IP.wrapIPv6(host), port, transport)); - iceServerBuilder.setTlsCertPolicy(PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK); - if (username != null && password != null) { - iceServerBuilder.setUsername(username); - iceServerBuilder.setPassword(password); - } else if (Arrays.asList("turn", "turns").contains(type)) { - //The WebRTC spec requires throwing an InvalidAccessError when username (from libwebrtc source coder) - //https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": skipping " + type + "/" + transport + " without username and password"); - continue; - } - final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": discovered ICE Server: " + iceServer); - listBuilder.add(iceServer); } } - } - } - final List iceServers = listBuilder.build(); - if (iceServers.size() == 0) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no ICE server found " + response); - } - onIceServersDiscovered.onIceServersDiscovered(iceServers); - }); + final List iceServers = listBuilder.build(); + if (iceServers.size() == 0) { + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": no ICE server found " + + response); + } + onIceServersDiscovered.onIceServersDiscovered(iceServers); + }); } else { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": has no external service discovery"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": has no external service discovery"); onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList()); } } @@ -1717,13 +2164,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia()); this.jingleConnectionManager.finishConnectionOrThrow(this); } else { - throw new IllegalStateException(String.format("Unable to call finish from %s", this.state)); + throw new IllegalStateException( + String.format("Unable to call finish from %s", this.state)); } } private void writeLogMessage(final State state) { final long duration = getCallDuration(); - if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { + if (state == State.TERMINATED_SUCCESS + || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { writeLogMessageSuccess(duration); } else { writeLogMessageMissed(); @@ -1777,18 +2226,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public void fireStateUpdate() { final RtpEndUserState endUserState = getEndUserState(); - xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState); + xmppConnectionService.notifyJingleRtpConnectionUpdate( + id.account, id.with, id.sessionId, endUserState); } private interface OnIceServersDiscovered { void onIceServersDiscovered(List iceServers); } - - private static class StateTransitionException extends Exception { - private final State state; - - private StateTransitionException(final State state) { - this.state = state; - } - } }