~singpolyma/cheogram-android

304205b2e344ae1c1b6b17e230589109a230b121 — Daniel Gultsch 1 year, 4 months ago 59ea66c
take senders attr into account when converting to and from sdp
M src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java => src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +2 -2
@@ 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();

M src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java => src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +9 -18
@@ 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;

M src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java => src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +7 -7
@@ 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);

M src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java => src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +85 -19
@@ 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();
        }
    }
}

M src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java => src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +110 -48
@@ 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;
        }
    }

}

M src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java => src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +61 -13
@@ 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));
            }
        }
    }
}