take senders attr into account when converting to and from sdp

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java            |   4 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java |  27 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java          |  14 
src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java                | 104 
src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java           | 158 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java              |  74 
6 files changed, 274 insertions(+), 107 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -1272,7 +1272,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
             }
             descriptionTransportBuilder.put(
                     content.getKey(),
-                    new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo)
+                    new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
             );
         }
         return Futures.immediateFuture(
@@ -1306,7 +1306,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
             omemoVerification.setOrEnsureEqual(decryptedTransport);
             descriptionTransportBuilder.put(
                     content.getKey(),
-                    new RtpContentMap.DescriptionTransport(descriptionTransport.description, decryptedTransport.payload)
+                    new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
             );
         }
         processPostponed();

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java 🔗

@@ -577,8 +577,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
 
     private void sendInitRequest() {
         final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE);
-        final Content content = new Content(this.contentCreator, this.contentName);
-        content.setSenders(this.contentSenders);
+        final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
         if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) {
             Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET");
             final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
@@ -656,8 +655,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         gatherAndConnectDirectCandidates();
         this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> {
             final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
-            final Content content = new Content(contentCreator, contentName);
-            content.setSenders(this.contentSenders);
+            final Content content = new Content(contentCreator, contentSenders, contentName);
             content.setDescription(this.description);
             if (success && candidate != null && !equalCandidateExists(candidate)) {
                 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
@@ -696,8 +694,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
     private void sendAcceptIbb() {
         this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
         final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
-        final Content content = new Content(contentCreator, contentName);
-        content.setSenders(this.contentSenders);
+        final Content content = new Content(contentCreator, contentSenders, contentName);
         content.setDescription(this.description);
         content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
         packet.addJingleContent(content);
@@ -910,8 +907,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
     private void sendFallbackToIbb() {
         Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb");
         final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE);
-        final Content content = new Content(this.contentCreator, this.contentName);
-        content.setSenders(this.contentSenders);
+        final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
         this.transportId = JingleConnectionManager.nextRandomId();
         content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
         packet.addJingleContent(content);
@@ -944,8 +940,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
 
         final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT);
 
-        final Content content = new Content(contentCreator, contentName);
-        content.setSenders(this.contentSenders);
+        final Content content = new Content(contentCreator, contentSenders, contentName);
         content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
         answer.addJingleContent(content);
 
@@ -1124,8 +1119,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
 
     private void sendProxyActivated(String cid) {
         final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
-        final Content content = new Content(this.contentCreator, this.contentName);
-        content.setSenders(this.contentSenders);
+        final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
         content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid)));
         packet.addJingleContent(content);
         this.sendJinglePacket(packet);
@@ -1133,8 +1127,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
 
     private void sendProxyError() {
         final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
-        final Content content = new Content(this.contentCreator, this.contentName);
-        content.setSenders(this.contentSenders);
+        final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
         content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error")));
         packet.addJingleContent(content);
         this.sendJinglePacket(packet);
@@ -1142,8 +1135,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
 
     private void sendCandidateUsed(final String cid) {
         JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
-        final Content content = new Content(this.contentCreator, this.contentName);
-        content.setSenders(this.contentSenders);
+        final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
         content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid)));
         packet.addJingleContent(content);
         this.sentCandidate = true;
@@ -1156,8 +1148,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
     private void sendCandidateError() {
         Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error");
         JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
-        Content content = new Content(this.contentCreator, this.contentName);
-        content.setSenders(this.contentSenders);
+        Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
         content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error")));
         packet.addJingleContent(content);
         this.sentCandidate = true;

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java 🔗

@@ -425,7 +425,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
             final RtpContentMap restartContentMap,
             final boolean isOffer)
             throws ExecutionException, InterruptedException {
-        final SessionDescription sessionDescription = SessionDescription.of(restartContentMap);
+        final SessionDescription sessionDescription = SessionDescription.of(restartContentMap, !isInitiator());
         final org.webrtc.SessionDescription.Type type =
                 isOffer
                         ? org.webrtc.SessionDescription.Type.OFFER
@@ -444,7 +444,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
         if (isOffer) {
             webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
             final SessionDescription localSessionDescription = setLocalSessionDescription();
-            setLocalContentMap(RtpContentMap.of(localSessionDescription));
+            setLocalContentMap(RtpContentMap.of(localSessionDescription, isInitiator()));
             // We need to respond OK before sending any candidates
             respondOk(jinglePacket);
             webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
@@ -726,7 +726,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
         this.storePeerDtlsSetup(contentMap.getDtlsSetup());
         final SessionDescription sessionDescription;
         try {
-            sessionDescription = SessionDescription.of(contentMap);
+            sessionDescription = SessionDescription.of(contentMap, false);
         } catch (final IllegalArgumentException | NullPointerException e) {
             Log.d(
                     Config.LOGTAG,
@@ -763,7 +763,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
         }
         final SessionDescription offer;
         try {
-            offer = SessionDescription.of(rtpContentMap);
+            offer = SessionDescription.of(rtpContentMap, true);
         } catch (final IllegalArgumentException | NullPointerException e) {
             Log.d(
                     Config.LOGTAG,
@@ -838,7 +838,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
             final org.webrtc.SessionDescription webRTCSessionDescription) {
         final SessionDescription sessionDescription =
                 SessionDescription.parse(webRTCSessionDescription.description);
-        final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
+        final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false);
         this.responderRtpContentMap = respondingRtpContentMap;
         storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip());
         webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
@@ -1289,7 +1289,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
             final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) {
         final SessionDescription sessionDescription =
                 SessionDescription.parse(webRTCSessionDescription.description);
-        final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
+        final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true);
         this.initiatorRtpContentMap = rtpContentMap;
         //TODO delay ready to receive ice until after session-init
         this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
@@ -1922,7 +1922,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
             sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
             return;
         }
-        final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
+        final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, isInitiator());
         final RtpContentMap transportInfo = rtpContentMap.transportInfo();
         final JinglePacket jinglePacket =
                 transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);

src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java 🔗

@@ -1,5 +1,6 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.collect.Collections2;
@@ -15,6 +16,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import javax.annotation.Nonnull;
+
 import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
 import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
@@ -58,13 +61,15 @@ public class RtpContentMap {
         return true;
     }
 
-    public static RtpContentMap of(final SessionDescription sessionDescription) {
+    public static RtpContentMap of(
+            final SessionDescription sessionDescription, final boolean isInitiator) {
         final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
                 new ImmutableMap.Builder<>();
         for (SessionDescription.Media media : sessionDescription.media) {
             final String id = Iterables.getFirst(media.attributes.get("mid"), null);
             Preconditions.checkNotNull(id, "media has no mid");
-            contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media));
+            contentMapBuilder.put(
+                    id, DescriptionTransport.of(sessionDescription, isInitiator, media));
         }
         final String groupAttribute =
                 Iterables.getFirst(sessionDescription.attributes.get("group"), null);
@@ -140,11 +145,16 @@ public class RtpContentMap {
             jinglePacket.addGroup(this.group);
         }
         for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
-            final Content content = new Content(Content.Creator.INITIATOR, entry.getKey());
-            if (entry.getValue().description != null) {
-                content.addChild(entry.getValue().description);
+            final DescriptionTransport descriptionTransport = entry.getValue();
+            final Content content =
+                    new Content(
+                            Content.Creator.INITIATOR,
+                            descriptionTransport.senders,
+                            entry.getKey());
+            if (descriptionTransport.description != null) {
+                content.addChild(descriptionTransport.description);
             }
-            content.addChild(entry.getValue().transport);
+            content.addChild(descriptionTransport.transport);
             jinglePacket.addJingleContent(content);
         }
         return jinglePacket;
@@ -163,7 +173,10 @@ public class RtpContentMap {
         newTransportInfo.addChild(candidate);
         return new RtpContentMap(
                 null,
-                ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
+                ImmutableMap.of(
+                        contentName,
+                        new DescriptionTransport(
+                                descriptionTransport.senders, null, newTransportInfo)));
     }
 
     RtpContentMap transportInfo() {
@@ -171,7 +184,9 @@ public class RtpContentMap {
                 null,
                 Maps.transformValues(
                         contents,
-                        dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())));
+                        dt ->
+                                new DescriptionTransport(
+                                        dt.senders, null, dt.transport.cloneWrapper())));
     }
 
     public IceUdpTransportInfo.Credentials getDistinctCredentials() {
@@ -179,7 +194,8 @@ public class RtpContentMap {
         final IceUdpTransportInfo.Credentials credentials =
                 Iterables.getFirst(allCredentials, null);
         if (allCredentials.size() == 1 && credentials != null) {
-            if (Strings.isNullOrEmpty(credentials.password) || Strings.isNullOrEmpty(credentials.ufrag)) {
+            if (Strings.isNullOrEmpty(credentials.password)
+                    || Strings.isNullOrEmpty(credentials.ufrag)) {
                 throw new IllegalStateException("Credentials are missing password or ufrag");
             }
             return credentials;
@@ -233,23 +249,45 @@ public class RtpContentMap {
         final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
                 new ImmutableMap.Builder<>();
         for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
-            final RtpDescription rtpDescription = content.getValue().description;
-            IceUdpTransportInfo transportInfo = content.getValue().transport;
+            final DescriptionTransport descriptionTransport = content.getValue();
+            final RtpDescription rtpDescription = descriptionTransport.description;
+            final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
             final IceUdpTransportInfo modifiedTransportInfo =
                     transportInfo.modifyCredentials(credentials, setup);
             contentMapBuilder.put(
                     content.getKey(),
-                    new DescriptionTransport(rtpDescription, modifiedTransportInfo));
+                    new DescriptionTransport(
+                            descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
         }
         return new RtpContentMap(this.group, contentMapBuilder.build());
     }
 
+    public Diff diff(final RtpContentMap rtpContentMap) {
+        final Set<String> existingContentIds = this.contents.keySet();
+        final Set<String> newContentIds = rtpContentMap.contents.keySet();
+        return new Diff(
+                Sets.difference(newContentIds, existingContentIds),
+                Sets.difference(existingContentIds, newContentIds));
+    }
+
+    public boolean iceRestart(final RtpContentMap rtpContentMap) {
+        try {
+            return !getDistinctCredentials().equals(rtpContentMap.getDistinctCredentials());
+        } catch (final IllegalStateException e) {
+            return false;
+        }
+    }
+
     public static class DescriptionTransport {
+        public final Content.Senders senders;
         public final RtpDescription description;
         public final IceUdpTransportInfo transport;
 
         public DescriptionTransport(
-                final RtpDescription description, final IceUdpTransportInfo transport) {
+                final Content.Senders senders,
+                final RtpDescription description,
+                final IceUdpTransportInfo transport) {
+            this.senders = senders;
             this.description = description;
             this.transport = transport;
         }
@@ -257,6 +295,7 @@ public class RtpContentMap {
         public static DescriptionTransport of(final Content content) {
             final GenericDescription description = content.getDescription();
             final GenericTransportInfo transportInfo = content.getTransport();
+            final Content.Senders senders = content.getSenders();
             final RtpDescription rtpDescription;
             final IceUdpTransportInfo iceUdpTransportInfo;
             if (description == null) {
@@ -274,22 +313,26 @@ public class RtpContentMap {
                         "Content does not contain ICE-UDP transport");
             }
             return new DescriptionTransport(
-                    rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
+                    senders,
+                    rtpDescription,
+                    OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
         }
 
-        public static DescriptionTransport of(
-                final SessionDescription sessionDescription, final SessionDescription.Media media) {
+        private static DescriptionTransport of(
+                final SessionDescription sessionDescription,
+                final boolean isInitiator,
+                final SessionDescription.Media media) {
+            final Content.Senders senders = Content.Senders.of(media, isInitiator);
             final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
             final IceUdpTransportInfo transportInfo =
                     IceUdpTransportInfo.of(sessionDescription, media);
-            return new DescriptionTransport(rtpDescription, transportInfo);
+            return new DescriptionTransport(senders, rtpDescription, transportInfo);
         }
 
         public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
             return ImmutableMap.copyOf(
                     Maps.transformValues(
-                            contents,
-                            content -> content == null ? null : of(content)));
+                            contents, content -> content == null ? null : of(content)));
         }
     }
 
@@ -304,4 +347,27 @@ public class RtpContentMap {
             super(message);
         }
     }
+
+    public static final class Diff {
+        public final Set<String> added;
+        public final Set<String> removed;
+
+        private Diff(final Set<String> added, final Set<String> removed) {
+            this.added = added;
+            this.removed = removed;
+        }
+
+        public boolean hasModifications() {
+            return !this.added.isEmpty() || !this.removed.isEmpty();
+        }
+
+        @Override
+        @Nonnull
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                    .add("added", added)
+                    .add("removed", removed)
+                    .toString();
+        }
+    }
 }

src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java 🔗

@@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle;
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.NonNull;
+
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
@@ -21,11 +23,12 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 
 public class SessionDescription {
 
-    public final static String LINE_DIVIDER = "\r\n";
-    private final static String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; //probably only true for DTLS-SRTP aka when we have a fingerprint
-    private final static int HARDCODED_MEDIA_PORT = 9;
-    private final static String HARDCODED_ICE_OPTIONS = "trickle";
-    private final static String HARDCODED_CONNECTION = "IN IP4 0.0.0.0";
+    public static final String LINE_DIVIDER = "\r\n";
+    private static final String HARDCODED_MEDIA_PROTOCOL =
+            "UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint
+    private static final int HARDCODED_MEDIA_PORT = 9;
+    private static final String HARDCODED_ICE_OPTIONS = "trickle";
+    private static final String HARDCODED_CONNECTION = "IN IP4 0.0.0.0";
 
     public final int version;
     public final String name;
@@ -33,8 +36,12 @@ public class SessionDescription {
     public final ArrayListMultimap<String, String> attributes;
     public final List<Media> media;
 
-
-    public SessionDescription(int version, String name, String connectionData, ArrayListMultimap<String, String> attributes, List<Media> media) {
+    public SessionDescription(
+            int version,
+            String name,
+            String connectionData,
+            ArrayListMultimap<String, String> attributes,
+            List<Media> media) {
         this.version = version;
         this.name = name;
         this.connectionData = connectionData;
@@ -42,7 +49,8 @@ public class SessionDescription {
         this.media = media;
     }
 
-    private static void appendAttributes(StringBuilder s, ArrayListMultimap<String, String> attributes) {
+    private static void appendAttributes(
+            StringBuilder s, ArrayListMultimap<String, String> attributes) {
         for (Map.Entry<String, String> attribute : attributes.entries()) {
             final String key = attribute.getKey();
             final String value = attribute.getValue();
@@ -109,7 +117,6 @@ public class SessionDescription {
                     }
                     break;
             }
-
         }
         if (currentMediaBuilder != null) {
             currentMediaBuilder.setAttributes(attributeMap);
@@ -121,7 +128,7 @@ public class SessionDescription {
         return sessionDescriptionBuilder.createSessionDescription();
     }
 
-    public static SessionDescription of(final RtpContentMap contentMap) {
+    public static SessionDescription of(final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
         final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
         final ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
         final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>();
@@ -129,12 +136,17 @@ public class SessionDescription {
         if (group != null) {
             final String semantics = group.getSemantics();
             checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
-            attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(group.getIdentificationTags()));
+            attributeMap.put(
+                    "group",
+                    group.getSemantics()
+                            + " "
+                            + Joiner.on(' ').join(group.getIdentificationTags()));
         }
 
         attributeMap.put("msid-semantic", " WMS my-media-stream");
 
-        for (final Map.Entry<String, RtpContentMap.DescriptionTransport> entry : contentMap.contents.entrySet()) {
+        for (final Map.Entry<String, RtpContentMap.DescriptionTransport> entry :
+                contentMap.contents.entrySet()) {
             final String name = entry.getKey();
             RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue();
             RtpDescription description = descriptionTransport.description;
@@ -143,19 +155,22 @@ public class SessionDescription {
             final String ufrag = transport.getAttribute("ufrag");
             final String pwd = transport.getAttribute("pwd");
             if (Strings.isNullOrEmpty(ufrag)) {
-                throw new IllegalArgumentException("Transport element is missing required ufrag attribute");
+                throw new IllegalArgumentException(
+                        "Transport element is missing required ufrag attribute");
             }
             checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces");
             mediaAttributes.put("ice-ufrag", ufrag);
             if (Strings.isNullOrEmpty(pwd)) {
-                throw new IllegalArgumentException("Transport element is missing required pwd attribute");
+                throw new IllegalArgumentException(
+                        "Transport element is missing required pwd attribute");
             }
             checkNoWhitespace(pwd, "pwd value must not contain any whitespaces");
             mediaAttributes.put("ice-pwd", pwd);
             mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS);
             final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
             if (fingerprint != null) {
-                mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent());
+                mediaAttributes.put(
+                        "fingerprint", fingerprint.getHash() + " " + fingerprint.getContent());
                 final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
                 if (setup != null) {
                     mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
@@ -174,37 +189,56 @@ public class SessionDescription {
                 mediaAttributes.put("rtpmap", payloadType.toSdpAttribute());
                 final List<RtpDescription.Parameter> parameters = payloadType.getParameters();
                 if (parameters.size() == 1) {
-                    mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0)));
+                    mediaAttributes.put(
+                            "fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0)));
                 } else if (parameters.size() > 0) {
-                    mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters));
+                    mediaAttributes.put(
+                            "fmtp", RtpDescription.Parameter.toSdpString(id, parameters));
                 }
-                for (RtpDescription.FeedbackNegotiation feedbackNegotiation : payloadType.getFeedbackNegotiations()) {
+                for (RtpDescription.FeedbackNegotiation feedbackNegotiation :
+                        payloadType.getFeedbackNegotiations()) {
                     final String type = feedbackNegotiation.getType();
                     final String subtype = feedbackNegotiation.getSubType();
                     if (Strings.isNullOrEmpty(type)) {
-                        throw new IllegalArgumentException("a feedback for payload-type " + id + " negotiation is missing type");
+                        throw new IllegalArgumentException(
+                                "a feedback for payload-type "
+                                        + id
+                                        + " negotiation is missing type");
                     }
-                    checkNoWhitespace(type, "feedback negotiation type must not contain whitespace");
-                    mediaAttributes.put("rtcp-fb", id + " " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
+                    checkNoWhitespace(
+                            type, "feedback negotiation type must not contain whitespace");
+                    mediaAttributes.put(
+                            "rtcp-fb",
+                            id
+                                    + " "
+                                    + type
+                                    + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
                 }
-                for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : payloadType.feedbackNegotiationTrrInts()) {
-                    mediaAttributes.put("rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue());
+                for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt :
+                        payloadType.feedbackNegotiationTrrInts()) {
+                    mediaAttributes.put(
+                            "rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue());
                 }
             }
 
-            for (RtpDescription.FeedbackNegotiation feedbackNegotiation : description.getFeedbackNegotiations()) {
+            for (RtpDescription.FeedbackNegotiation feedbackNegotiation :
+                    description.getFeedbackNegotiations()) {
                 final String type = feedbackNegotiation.getType();
                 final String subtype = feedbackNegotiation.getSubType();
                 if (Strings.isNullOrEmpty(type)) {
                     throw new IllegalArgumentException("a feedback negotiation is missing type");
                 }
                 checkNoWhitespace(type, "feedback negotiation type must not contain whitespace");
-                mediaAttributes.put("rtcp-fb", "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
+                mediaAttributes.put(
+                        "rtcp-fb",
+                        "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
             }
-            for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) {
+            for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt :
+                    description.feedbackNegotiationTrrInts()) {
                 mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue());
             }
-            for (final RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) {
+            for (final RtpDescription.RtpHeaderExtension extension :
+                    description.getHeaderExtensions()) {
                 final String id = extension.getId();
                 final String uri = extension.getUri();
                 if (Strings.isNullOrEmpty(id)) {
@@ -218,7 +252,8 @@ public class SessionDescription {
                 mediaAttributes.put("extmap", id + " " + uri);
             }
 
-            if (description.hasChild("extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) {
+            if (description.hasChild(
+                    "extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) {
                 mediaAttributes.put("extmap-allow-mixed", "");
             }
 
@@ -226,13 +261,16 @@ public class SessionDescription {
                 final String semantics = sourceGroup.getSemantics();
                 final List<String> groups = sourceGroup.getSsrcs();
                 if (Strings.isNullOrEmpty(semantics)) {
-                    throw new IllegalArgumentException("A SSRC group is missing semantics attribute");
+                    throw new IllegalArgumentException(
+                            "A SSRC group is missing semantics attribute");
                 }
                 checkNoWhitespace(semantics, "source group semantics must not contain whitespace");
                 if (groups.size() == 0) {
                     throw new IllegalArgumentException("A SSRC group is missing SSRC ids");
                 }
-                mediaAttributes.put("ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups)));
+                mediaAttributes.put(
+                        "ssrc-group",
+                        String.format("%s %s", semantics, Joiner.on(' ').join(groups)));
             }
             for (final RtpDescription.Source source : description.getSources()) {
                 for (final RtpDescription.Source.Parameter parameter : source.getParameters()) {
@@ -240,14 +278,18 @@ public class SessionDescription {
                     final String parameterName = parameter.getParameterName();
                     final String parameterValue = parameter.getParameterValue();
                     if (Strings.isNullOrEmpty(id)) {
-                        throw new IllegalArgumentException("A source specific media attribute is missing the id");
+                        throw new IllegalArgumentException(
+                                "A source specific media attribute is missing the id");
                     }
-                    checkNoWhitespace(id, "A source specific media attributes must not contain whitespaces");
+                    checkNoWhitespace(
+                            id, "A source specific media attributes must not contain whitespaces");
                     if (Strings.isNullOrEmpty(parameterName)) {
-                        throw new IllegalArgumentException("A source specific media attribute is missing its name");
+                        throw new IllegalArgumentException(
+                                "A source specific media attribute is missing its name");
                     }
                     if (Strings.isNullOrEmpty(parameterValue)) {
-                        throw new IllegalArgumentException("A source specific media attribute is missing its value");
+                        throw new IllegalArgumentException(
+                                "A source specific media attribute is missing its value");
                     }
                     mediaAttributes.put("ssrc", id + " " + parameterName + ":" + parameterValue);
                 }
@@ -255,14 +297,14 @@ public class SessionDescription {
 
             mediaAttributes.put("mid", name);
 
-            //random additional attributes
-            mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0");
-            mediaAttributes.put("sendrecv", "");
-
+            mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), "");
             if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) {
                 mediaAttributes.put("rtcp-mux", "");
             }
 
+            // random additional attributes
+            mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0");
+
             final MediaBuilder mediaBuilder = new MediaBuilder();
             mediaBuilder.setMedia(description.getMedia().toString().toLowerCase(Locale.ROOT));
             mediaBuilder.setConnectionData(HARDCODED_CONNECTION);
@@ -271,7 +313,6 @@ public class SessionDescription {
             mediaBuilder.setAttributes(mediaAttributes);
             mediaBuilder.setFormats(formatBuilder.build());
             mediaListBuilder.add(mediaBuilder.createMedia());
-
         }
         sessionDescriptionBuilder.setVersion(0);
         sessionDescriptionBuilder.setName("-");
@@ -317,17 +358,33 @@ public class SessionDescription {
         }
     }
 
+    @NonNull
     @Override
     public String toString() {
-        final StringBuilder s = new StringBuilder()
-                .append("v=").append(version).append(LINE_DIVIDER)
-                //TODO randomize or static
-                .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1").append(LINE_DIVIDER) //what ever that means
-                .append("s=").append(name).append(LINE_DIVIDER)
-                .append("t=0 0").append(LINE_DIVIDER);
+        final StringBuilder s =
+                new StringBuilder()
+                        .append("v=")
+                        .append(version)
+                        .append(LINE_DIVIDER)
+                        // TODO randomize or static
+                        .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1")
+                        .append(LINE_DIVIDER) // what ever that means
+                        .append("s=")
+                        .append(name)
+                        .append(LINE_DIVIDER)
+                        .append("t=0 0")
+                        .append(LINE_DIVIDER);
         appendAttributes(s, attributes);
         for (Media media : this.media) {
-            s.append("m=").append(media.media).append(' ').append(media.port).append(' ').append(media.protocol).append(' ').append(Joiner.on(' ').join(media.formats)).append(LINE_DIVIDER);
+            s.append("m=")
+                    .append(media.media)
+                    .append(' ')
+                    .append(media.port)
+                    .append(' ')
+                    .append(media.protocol)
+                    .append(' ')
+                    .append(Joiner.on(' ').join(media.formats))
+                    .append(LINE_DIVIDER);
             s.append("c=").append(media.connectionData).append(LINE_DIVIDER);
             appendAttributes(s, media.attributes);
         }
@@ -342,7 +399,13 @@ public class SessionDescription {
         public final String connectionData;
         public final ArrayListMultimap<String, String> attributes;
 
-        public Media(String media, int port, String protocol, List<Integer> formats, String connectionData, ArrayListMultimap<String, String> attributes) {
+        public Media(
+                String media,
+                int port,
+                String protocol,
+                List<Integer> formats,
+                String connectionData,
+                ArrayListMultimap<String, String> attributes) {
             this.media = media;
             this.port = port;
             this.protocol = protocol;
@@ -351,5 +414,4 @@ public class SessionDescription {
             this.attributes = attributes;
         }
     }
-
 }

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java 🔗

@@ -1,20 +1,27 @@
 package eu.siacs.conversations.xmpp.jingle.stanzas;
 
+import android.util.Log;
+
 import androidx.annotation.NonNull;
 
 import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 
 import java.util.Locale;
+import java.util.Set;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.jingle.SessionDescription;
 
 public class Content extends Element {
 
-    public Content(final Creator creator, final String name) {
+    public Content(final Creator creator, final Senders senders, final String name) {
         super("content", Namespace.JINGLE);
         this.setAttribute("creator", creator.toString());
         this.setAttribute("name", name);
+        this.setSenders(senders);
     }
 
     private Content() {
@@ -38,11 +45,17 @@ public class Content extends Element {
     }
 
     public Senders getSenders() {
+        final String attribute = getAttribute("senders");
+        if (Strings.isNullOrEmpty(attribute)) {
+            return Senders.BOTH;
+        }
         return Senders.of(getAttribute("senders"));
     }
 
-    public void setSenders(Senders senders) {
-        this.setAttribute("senders", senders.toString());
+    public void setSenders(final Senders senders) {
+        if (senders != null && senders != Senders.BOTH) {
+            this.setAttribute("senders", senders.toString());
+        }
     }
 
     public GenericDescription getDescription() {
@@ -51,9 +64,7 @@ public class Content extends Element {
             return null;
         }
         final String namespace = description.getNamespace();
-        if (FileTransferDescription.NAMESPACES.contains(namespace)) {
-            return FileTransferDescription.upgrade(description);
-        } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
+        if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
             return RtpDescription.upgrade(description);
         } else {
             return GenericDescription.upgrade(description);
@@ -73,11 +84,7 @@ public class Content extends Element {
     public GenericTransportInfo getTransport() {
         final Element transport = this.findChild("transport");
         final String namespace = transport == null ? null : transport.getNamespace();
-        if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) {
-            return IbbTransportInfo.upgrade(transport);
-        } else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) {
-            return S5BTransportInfo.upgrade(transport);
-        } else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
+        if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
             return IceUdpTransportInfo.upgrade(transport);
         } else if (transport != null) {
             return GenericTransportInfo.upgrade(transport);
@@ -91,7 +98,8 @@ public class Content extends Element {
     }
 
     public enum Creator {
-        INITIATOR, RESPONDER;
+        INITIATOR,
+        RESPONDER;
 
         public static Creator of(final String value) {
             return Creator.valueOf(value.toUpperCase(Locale.ROOT));
@@ -105,16 +113,56 @@ public class Content extends Element {
     }
 
     public enum Senders {
-        BOTH, INITIATOR, NONE, RESPONDER;
+        BOTH,
+        INITIATOR,
+        NONE,
+        RESPONDER;
 
         public static Senders of(final String value) {
             return Senders.valueOf(value.toUpperCase(Locale.ROOT));
         }
 
+        public static Senders of(final SessionDescription.Media media, final boolean initiator) {
+            final Set<String> attributes = media.attributes.keySet();
+            if (attributes.contains("sendrecv")) {
+                return BOTH;
+            } else if (attributes.contains("inactive")) {
+                return NONE;
+            } else if (attributes.contains("sendonly")) {
+                return initiator ? INITIATOR : RESPONDER;
+            } else if (attributes.contains("recvonly")) {
+                return initiator ? RESPONDER : INITIATOR;
+            }
+            Log.w(Config.LOGTAG,"assuming default value for senders");
+            // If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is
+            // present, "sendrecv" SHOULD be assumed as the default
+            // https://www.rfc-editor.org/rfc/rfc4566
+            return BOTH;
+        }
+
         @Override
         @NonNull
         public String toString() {
             return super.toString().toLowerCase(Locale.ROOT);
         }
+
+        public String asMediaAttribute(final boolean initiator) {
+            final boolean responder = !initiator;
+            if (this == Content.Senders.BOTH) {
+                return "sendrecv";
+            } else if (this == Content.Senders.NONE) {
+                return "inactive";
+            } else if ((initiator && this == Content.Senders.INITIATOR)
+                    || (responder && this == Content.Senders.RESPONDER)) {
+                return "sendonly";
+            } else if ((initiator && this == Content.Senders.RESPONDER)
+                    || (responder && this == Content.Senders.INITIATOR)) {
+                return "recvonly";
+            } else {
+                throw new IllegalStateException(
+                        String.format(
+                                "illegal combination of initiator=%s and %s", initiator, this));
+            }
+        }
     }
 }