minor jingle code clean up

Daniel Gultsch created

Change summary

build.gradle                                                                                |   6 
src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java                            | 138 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java                   |  10 
src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java                         |   9 
src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java                    |  12 
src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java                         |  41 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java           |  14 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java                |  25 
src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java |  31 
9 files changed, 135 insertions(+), 151 deletions(-)

Detailed changes

build.gradle 🔗

@@ -96,8 +96,8 @@ dependencies {
     implementation "com.squareup.retrofit2:converter-gson:2.11.0"
     implementation "com.squareup.okhttp3:okhttp:4.12.0"
 
-    implementation 'com.google.guava:guava:32.1.3-android'
-    quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.35'
+    implementation 'com.google.guava:guava:33.4.0-android'
+    quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.52'
     implementation 'im.conversations.webrtc:webrtc-android:129.0.0'
 }
 
@@ -113,7 +113,7 @@ android {
     defaultConfig {
         minSdkVersion 23
         targetSdkVersion 34
-        versionCode 42134
+        versionCode 42135
         versionName "2.17.12"
         archivesBaseName += "-$versionName"
         applicationId "eu.siacs.conversations"

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

@@ -1,98 +1,94 @@
 package eu.siacs.conversations.xmpp.jingle;
 
 import android.util.Log;
-
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.primitives.Ints;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.utils.IP;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import im.conversations.android.xmpp.model.stanza.Iq;
-
-import org.webrtc.PeerConnection;
-
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
+import org.webrtc.PeerConnection;
 
 public final class IceServers {
 
-    public static List<PeerConnection.IceServer> parse(final Iq response) {
-        ImmutableList.Builder<PeerConnection.IceServer> listBuilder = new ImmutableList.Builder<>();
+    public static Set<PeerConnection.IceServer> parse(final Iq response) {
         if (response.getType() == Iq.Type.RESULT) {
-            final Element services =
-                    response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
-            final List<Element> 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) {
+            return Collections.emptySet();
+        }
+        final var builder = new ImmutableSet.Builder<PeerConnection.IceServer>();
+        final Element services =
+                response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
+        final List<Element> 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.w(
+                                Config.LOGTAG,
+                                "skipping invalid combination of udp/tls in external services");
                         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.w(
-                                    Config.LOGTAG,
-                                    "skipping invalid combination of udp/tls in external services");
-                            continue;
-                        }
-
-                        // STUN URLs do not support a query section since M110
-                        final String uri;
-                        if (Arrays.asList("stun", "stuns").contains(type)) {
-                            uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port);
-                        } else {
-                            uri =
-                                    String.format(
-                                            "%s:%s:%s?transport=%s",
-                                            type, IP.wrapIPv6(host), port, transport);
-                        }
+                    // STUN URLs do not support a query section since M110
+                    final String uri;
+                    if (Arrays.asList("stun", "stuns").contains(type)) {
+                        uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port);
+                    } else {
+                        uri =
+                                String.format(
+                                        "%s:%s:%s?transport=%s",
+                                        type, IP.wrapIPv6(host), port, transport);
+                    }
 
-                        final PeerConnection.IceServer.Builder iceServerBuilder =
-                                PeerConnection.IceServer.builder(uri);
-                        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.w(
-                                    Config.LOGTAG,
-                                    "skipping "
-                                            + type
-                                            + "/"
-                                            + transport
-                                            + " without username and password");
-                            continue;
-                        }
-                        final PeerConnection.IceServer iceServer =
-                                iceServerBuilder.createIceServer();
-                        Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer);
-                        listBuilder.add(iceServer);
+                    final PeerConnection.IceServer.Builder iceServerBuilder =
+                            PeerConnection.IceServer.builder(uri);
+                    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.w(
+                                Config.LOGTAG,
+                                "skipping "
+                                        + type
+                                        + "/"
+                                        + transport
+                                        + " without username and password");
+                        continue;
                     }
+                    final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer();
+                    Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer);
+                    builder.add(iceServer);
                 }
             }
         }
-        return listBuilder.build();
+        return builder.build();
     }
 }

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

@@ -1339,7 +1339,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     private synchronized void sendSessionAccept(
             final Set<Media> media,
             final SessionDescription offer,
-            final List<PeerConnection.IceServer> iceServers) {
+            final Set<PeerConnection.IceServer> iceServers) {
         if (isTerminated()) {
             Log.w(
                     Config.LOGTAG,
@@ -1823,7 +1823,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     private synchronized void sendSessionInitiate(
             final Set<Media> media,
             final State targetState,
-            final List<PeerConnection.IceServer> iceServers) {
+            final Set<PeerConnection.IceServer> iceServers) {
         if (isTerminated()) {
             Log.w(
                     Config.LOGTAG,
@@ -2320,7 +2320,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
     private void setupWebRTC(
             final Set<Media> media,
-            final List<PeerConnection.IceServer> iceServers,
+            final Set<PeerConnection.IceServer> iceServers,
             final boolean trickle)
             throws WebRTCWrapper.InitializationException {
         this.jingleConnectionManager.ensureConnectionIsRegistered(this);
@@ -2843,7 +2843,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
             Log.w(
                     Config.LOGTAG,
                     id.account.getJid().asBareJid() + ": has no external service discovery");
-            onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList());
+            onIceServersDiscovered.onIceServersDiscovered(Collections.emptySet());
         }
     }
 
@@ -2956,6 +2956,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private interface OnIceServersDiscovered {
-        void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
+        void onIceServersDiscovered(Set<PeerConnection.IceServer> iceServers);
     }
 }

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

@@ -12,7 +12,6 @@ import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-
 import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
 import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
@@ -21,13 +20,11 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 import im.conversations.android.xmpp.model.jingle.Jingle;
-
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-
 import javax.annotation.Nonnull;
 
 public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTransportInfo> {
@@ -99,7 +96,7 @@ public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTran
     }
 
     void requireDTLSFingerprint(final boolean requireActPass) {
-        if (this.contents.size() == 0) {
+        if (this.contents.isEmpty()) {
             throw new IllegalStateException("No contents available");
         }
         for (Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> entry :
@@ -118,7 +115,8 @@ public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTran
             if (setup == null) {
                 throw new SecurityException(
                         String.format(
-                                "Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute",
+                                "Use of DTLS-SRTP (XEP-0320) is required for content %s but missing"
+                                        + " setup attribute",
                                 entry.getKey()));
             }
             if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
@@ -127,6 +125,7 @@ public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTran
             }
         }
     }
+
     RtpContentMap transportInfo(
             final String contentName, final IceUdpTransportInfo.Candidate candidate) {
         final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =

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

@@ -2,9 +2,7 @@ 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;
@@ -12,7 +10,6 @@ import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Multimap;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
@@ -21,7 +18,6 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
-
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -206,6 +202,10 @@ public class SessionDescription {
                 entry : contentMap.contents.entrySet()) {
             final String name = entry.getKey();
             checkNoWhitespace(name, "content name must not contain any whitespace");
+            // https://groups.google.com/g/discuss-webrtc/c/VG406JMTBI4/m/MrSex_q7AgAJ
+            if (name.length() > 16) {
+                throw new IllegalArgumentException("mid should not be longer than 16 chars");
+            }
             final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
                     entry.getValue();
             final RtpDescription description = descriptionTransport.description;
@@ -226,7 +226,7 @@ public class SessionDescription {
                 if (parameters.size() == 1) {
                     mediaAttributes.put(
                             "fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0)));
-                } else if (parameters.size() > 0) {
+                } else if (!parameters.isEmpty()) {
                     mediaAttributes.put(
                             "fmtp", RtpDescription.Parameter.toSdpString(id, parameters));
                 }
@@ -306,7 +306,7 @@ public class SessionDescription {
                             "A SSRC group is missing semantics attribute");
                 }
                 checkNoWhitespace(semantics, "source group semantics must not contain whitespace");
-                if (groups.size() == 0) {
+                if (groups.isEmpty()) {
                     throw new IllegalArgumentException("A SSRC group is missing SSRC ids");
                 }
                 for (final String source : groups) {

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

@@ -3,18 +3,26 @@ package eu.siacs.conversations.xmpp.jingle;
 import android.content.Context;
 import android.os.Build;
 import android.util.Log;
-
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.SettableFuture;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.services.XmppConnectionService;
-
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import org.webrtc.AudioSource;
 import org.webrtc.AudioTrack;
 import org.webrtc.CandidatePairChangeEvent;
@@ -35,19 +43,6 @@ import org.webrtc.SessionDescription;
 import org.webrtc.VideoTrack;
 import org.webrtc.audio.JavaAudioDeviceModule;
 
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
 public class WebRTCWrapper {
 
     private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
@@ -235,13 +230,13 @@ public class WebRTCWrapper {
 
     synchronized void initializePeerConnection(
             final Set<Media> media,
-            final List<PeerConnection.IceServer> iceServers,
+            final Set<PeerConnection.IceServer> iceServers,
             final boolean trickle)
             throws InitializationException {
         Preconditions.checkState(this.eglBase != null);
         Preconditions.checkNotNull(media);
         Preconditions.checkArgument(
-                media.size() > 0, "media can not be empty when initializing peer connection");
+                !media.isEmpty(), "media can not be empty when initializing peer connection");
         final boolean setUseHardwareAcousticEchoCanceler =
                 !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL);
         Log.d(
@@ -371,16 +366,16 @@ public class WebRTCWrapper {
         if (videoSourceWrapper != null) {
             try {
                 videoSourceWrapper.stopCapture();
-            } catch (InterruptedException e) {
-                e.printStackTrace();
+            } catch (final InterruptedException e) {
+                Log.e(Config.LOGTAG, "could not stop capturing video source", e);
             }
         }
     }
 
     public static PeerConnection.RTCConfiguration buildConfiguration(
-            final List<PeerConnection.IceServer> iceServers, final boolean trickle) {
+            final Set<PeerConnection.IceServer> iceServers, final boolean trickle) {
         final PeerConnection.RTCConfiguration rtcConfig =
-                new PeerConnection.RTCConfiguration(iceServers);
+                new PeerConnection.RTCConfiguration(ImmutableList.copyOf(iceServers));
         rtcConfig.tcpCandidatePolicy =
                 PeerConnection.TcpCandidatePolicy.DISABLED; // XEP-0176 doesn't support tcp
         if (trickle) {
@@ -397,7 +392,7 @@ public class WebRTCWrapper {
     }
 
     void reconfigurePeerConnection(
-            final List<PeerConnection.IceServer> iceServers, final boolean trickle) {
+            final Set<PeerConnection.IceServer> iceServers, final boolean trickle) {
         requirePeerConnection().setConfiguration(buildConfiguration(iceServers, trickle));
     }
 

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

@@ -1,28 +1,23 @@
 package eu.siacs.conversations.xmpp.jingle.stanzas;
 
 import android.util.Log;
-
 import androidx.annotation.NonNull;
-
 import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
-import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimap;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.jingle.SessionDescription;
 import eu.siacs.conversations.xmpp.jingle.transports.Transport;
-
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -101,7 +96,9 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
         for (final Element child : this.children) {
             if (Namespace.JINGLE_TRANSPORT_ICE_OPTION.equals(child.getNamespace())
                     && IceOption.WELL_KNOWN.contains(child.getName())) {
-                optionBuilder.add(child.getName());
+                optionBuilder.add(
+                        SessionDescription.checkNoWhitespace(
+                                child.getName(), "Ice options should not contain whitespace"));
             }
         }
         return optionBuilder.build();
@@ -159,7 +156,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
         final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
         transportInfo.setAttributes(new Hashtable<>(getAttributes()));
         transportInfo.setChildren(this.getChildren());
-        for(final Candidate candidate : candidates) {
+        for (final Candidate candidate : candidates) {
             transportInfo.addChild(candidate);
         }
         return transportInfo;
@@ -220,7 +217,8 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
             return null;
         }
 
-        public static Candidate fromSdpAttributeValue(final String value, final String currentUfrag) {
+        public static Candidate fromSdpAttributeValue(
+                final String value, final String currentUfrag) {
             final String[] segments = value.split(" ");
             if (segments.length < 6) {
                 return null;

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

@@ -1,25 +1,22 @@
 package eu.siacs.conversations.xmpp.jingle.stanzas;
 
 import android.util.Pair;
-
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
-
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.jingle.Media;
+import eu.siacs.conversations.xmpp.jingle.SessionDescription;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.jingle.Media;
-import eu.siacs.conversations.xmpp.jingle.SessionDescription;
-
 public class RtpDescription extends GenericDescription {
 
     private RtpDescription(final String media) {
@@ -291,7 +288,7 @@ public class RtpDescription extends GenericDescription {
             final String channels = this.getAttribute("channels");
             if (channels == null) {
                 return 1; // The number of channels; if omitted, it MUST be assumed to contain one
-                          // channel
+                // channel
             }
             try {
                 return Integer.parseInt(channels);
@@ -544,13 +541,17 @@ public class RtpDescription extends GenericDescription {
         }
 
         public List<String> getSsrcs() {
-            ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
-            for (Element child : this.children) {
+            final ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
+            for (final Element child : this.children) {
                 if ("source".equals(child.getName())) {
                     final String ssrc = child.getAttribute("ssrc");
-                    if (ssrc != null) {
-                        builder.add(ssrc);
+                    if (Strings.isNullOrEmpty(ssrc)) {
+                        continue;
                     }
+                    builder.add(
+                            SessionDescription.checkNoNewline(
+                                    ssrc,
+                                    "Source Specific media attributes can not contain newline"));
                 }
             }
             return builder.build();

src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java 🔗

@@ -5,7 +5,6 @@ import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.logDescription;
 
 import android.content.Context;
 import android.util.Log;
-
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.Closeables;
@@ -13,7 +12,6 @@ import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.SettableFuture;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.xml.Namespace;
@@ -22,17 +20,7 @@ import eu.siacs.conversations.xmpp.jingle.IceServers;
 import eu.siacs.conversations.xmpp.jingle.WebRTCWrapper;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
-
 import im.conversations.android.xmpp.model.stanza.Iq;
-
-import org.webrtc.CandidatePairChangeEvent;
-import org.webrtc.DataChannel;
-import org.webrtc.IceCandidate;
-import org.webrtc.MediaStream;
-import org.webrtc.PeerConnection;
-import org.webrtc.PeerConnectionFactory;
-import org.webrtc.SessionDescription;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
@@ -46,14 +34,21 @@ import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Queue;
+import java.util.Set;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
-
 import javax.annotation.Nonnull;
+import org.webrtc.CandidatePairChangeEvent;
+import org.webrtc.DataChannel;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.SessionDescription;
 
 public class WebRTCDataChannelTransport implements Transport {
 
@@ -229,12 +224,12 @@ public class WebRTCDataChannelTransport implements Transport {
         }
     }
 
-    private ListenableFuture<List<PeerConnection.IceServer>> getIceServers() {
+    private ListenableFuture<Set<PeerConnection.IceServer>> getIceServers() {
         if (Config.DISABLE_PROXY_LOOKUP) {
-            return Futures.immediateFuture(Collections.emptyList());
+            return Futures.immediateFuture(Collections.emptySet());
         }
         if (xmppConnection.getFeatures().externalServiceDiscovery()) {
-            final SettableFuture<List<PeerConnection.IceServer>> iceServerFuture =
+            final SettableFuture<Set<PeerConnection.IceServer>> iceServerFuture =
                     SettableFuture.create();
             final Iq request = new Iq(Iq.Type.GET);
             request.setTo(this.account.getDomain());
@@ -254,12 +249,12 @@ public class WebRTCDataChannelTransport implements Transport {
                     });
             return iceServerFuture;
         } else {
-            return Futures.immediateFuture(Collections.emptyList());
+            return Futures.immediateFuture(Collections.emptySet());
         }
     }
 
     private PeerConnection createPeerConnection(
-            final List<PeerConnection.IceServer> iceServers, final boolean trickle) {
+            final Set<PeerConnection.IceServer> iceServers, final boolean trickle) {
         final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, trickle);
         final PeerConnection peerConnection =
                 requirePeerConnectionFactory()