JingleRtpConnection code clean up

This commit is contained in:
Daniel Gultsch 2023-11-13 12:54:55 +01:00
parent 1471969237
commit 80c49955f0
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2

View file

@ -18,7 +18,6 @@ import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
@ -26,23 +25,6 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.PeerConnection;
import org.webrtc.VideoTrack;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@ -61,7 +43,6 @@ import eu.siacs.conversations.utils.IP;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
@ -73,6 +54,23 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.PeerConnection;
import org.webrtc.VideoTrack;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class JingleRtpConnection extends AbstractJingleConnection public class JingleRtpConnection extends AbstractJingleConnection
implements WebRTCWrapper.EventCallback { implements WebRTCWrapper.EventCallback {
@ -195,64 +193,37 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
private static State reasonToState(Reason reason) { private static State reasonToState(Reason reason) {
switch (reason) { return switch (reason) {
case SUCCESS: case SUCCESS -> State.TERMINATED_SUCCESS;
return State.TERMINATED_SUCCESS; case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
case DECLINE: case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
case BUSY: case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
return State.TERMINATED_DECLINED_OR_BUSY; case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
case CANCEL: .TERMINATED_APPLICATION_FAILURE;
case TIMEOUT: default -> State.TERMINATED_CONNECTIVITY_ERROR;
return State.TERMINATED_CANCEL_OR_TIMEOUT; };
case SECURITY_ERROR:
return State.TERMINATED_SECURITY_ERROR;
case FAILED_APPLICATION:
case UNSUPPORTED_TRANSPORTS:
case UNSUPPORTED_APPLICATIONS:
return State.TERMINATED_APPLICATION_FAILURE;
default:
return State.TERMINATED_CONNECTIVITY_ERROR;
}
} }
@Override @Override
synchronized void deliverPacket(final JinglePacket jinglePacket) { synchronized void deliverPacket(final JinglePacket jinglePacket) {
switch (jinglePacket.getAction()) { switch (jinglePacket.getAction()) {
case SESSION_INITIATE: case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket);
receiveSessionInitiate(jinglePacket); case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket);
break; case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket);
case TRANSPORT_INFO: case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket);
receiveTransportInfo(jinglePacket); case CONTENT_ADD -> receiveContentAdd(jinglePacket);
break; case CONTENT_ACCEPT -> receiveContentAccept(jinglePacket);
case SESSION_ACCEPT: case CONTENT_REJECT -> receiveContentReject(jinglePacket);
receiveSessionAccept(jinglePacket); case CONTENT_REMOVE -> receiveContentRemove(jinglePacket);
break; case CONTENT_MODIFY -> receiveContentModify(jinglePacket);
case SESSION_TERMINATE: default -> {
receiveSessionTerminate(jinglePacket);
break;
case CONTENT_ADD:
receiveContentAdd(jinglePacket);
break;
case CONTENT_ACCEPT:
receiveContentAccept(jinglePacket);
break;
case CONTENT_REJECT:
receiveContentReject(jinglePacket);
break;
case CONTENT_REMOVE:
receiveContentRemove(jinglePacket);
break;
case CONTENT_MODIFY:
receiveContentModify(jinglePacket);
break;
default:
respondOk(jinglePacket); respondOk(jinglePacket);
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
String.format( String.format(
"%s: received unhandled jingle action %s", "%s: received unhandled jingle action %s",
id.account.getJid().asBareJid(), jinglePacket.getAction())); id.account.getJid().asBareJid(), jinglePacket.getAction()));
break; }
} }
} }
@ -354,15 +325,22 @@ public class JingleRtpConnection extends AbstractJingleConnection
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates =
contentMap.contents.entrySet(); contentMap.contents.entrySet();
final RtpContentMap remote = getRemoteContentMap(); final RtpContentMap remote = getRemoteContentMap();
final Set<String> remoteContentIds = remote == null ? Collections.emptySet() : remote.contents.keySet(); final Set<String> remoteContentIds =
remote == null ? Collections.emptySet() : remote.contents.keySet();
if (Collections.disjoint(remoteContentIds, contentMap.contents.keySet())) { if (Collections.disjoint(remoteContentIds, contentMap.contents.keySet())) {
Log.d(Config.LOGTAG,"received transport-info for unknown contents "+contentMap.contents.keySet()+" (known: "+remoteContentIds+")"); Log.d(
Config.LOGTAG,
"received transport-info for unknown contents "
+ contentMap.contents.keySet()
+ " (known: "
+ remoteContentIds
+ ")");
respondOk(jinglePacket); respondOk(jinglePacket);
pendingIceCandidates.addAll(candidates); pendingIceCandidates.addAll(candidates);
return; return;
} }
if (this.state != State.SESSION_ACCEPTED) { if (this.state != State.SESSION_ACCEPTED) {
Log.d(Config.LOGTAG,"received transport-info prematurely. adding to backlog"); Log.d(Config.LOGTAG, "received transport-info prematurely. adding to backlog");
respondOk(jinglePacket); respondOk(jinglePacket);
pendingIceCandidates.addAll(candidates); pendingIceCandidates.addAll(candidates);
return; return;
@ -401,8 +379,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
final boolean hasFullTransportInfo = modification.hasFullTransportInfo(); final boolean hasFullTransportInfo = modification.hasFullTransportInfo();
final ListenableFuture<RtpContentMap> future = final ListenableFuture<RtpContentMap> future =
receiveRtpContentMap( receiveRtpContentMap(
modification, this.omemoVerification.hasFingerprint() && hasFullTransportInfo); modification,
Futures.addCallback(future, new FutureCallback<RtpContentMap>() { this.omemoVerification.hasFingerprint() && hasFullTransportInfo);
Futures.addCallback(
future,
new FutureCallback<>() {
@Override @Override
public void onSuccess(final RtpContentMap rtpContentMap) { public void onSuccess(final RtpContentMap rtpContentMap) {
receiveContentAdd(jinglePacket, rtpContentMap); receiveContentAdd(jinglePacket, rtpContentMap);
@ -418,9 +399,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ ": improperly formatted contents in content-add", + ": improperly formatted contents in content-add",
throwable); throwable);
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); sendSessionTerminate(
Reason.ofThrowable(rootCause), rootCause.getMessage());
} }
}, MoreExecutors.directExecutor()); },
MoreExecutors.directExecutor());
} else { } else {
terminateWithOutOfOrder(jinglePacket); terminateWithOutOfOrder(jinglePacket);
} }
@ -508,8 +491,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
final boolean hasFullTransportInfo = receivedContentAccept.hasFullTransportInfo(); final boolean hasFullTransportInfo = receivedContentAccept.hasFullTransportInfo();
final ListenableFuture<RtpContentMap> future = final ListenableFuture<RtpContentMap> future =
receiveRtpContentMap( receiveRtpContentMap(
receivedContentAccept, this.omemoVerification.hasFingerprint() && hasFullTransportInfo); receivedContentAccept,
Futures.addCallback(future, new FutureCallback<RtpContentMap>() { this.omemoVerification.hasFingerprint() && hasFullTransportInfo);
Futures.addCallback(
future,
new FutureCallback<>() {
@Override @Override
public void onSuccess(final RtpContentMap result) { public void onSuccess(final RtpContentMap result) {
receiveContentAccept(result); receiveContentAccept(result);
@ -518,9 +504,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
@Override @Override
public void onFailure(@NonNull final Throwable throwable) { public void onFailure(@NonNull final Throwable throwable) {
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); sendSessionTerminate(
Reason.ofThrowable(throwable), throwable.getMessage());
} }
}, MoreExecutors.directExecutor()); },
MoreExecutors.directExecutor());
} else { } else {
Log.d(Config.LOGTAG, "received content-accept did not match our outgoing content-add"); Log.d(Config.LOGTAG, "received content-accept did not match our outgoing content-add");
terminateWithOutOfOrder(jinglePacket); terminateWithOutOfOrder(jinglePacket);
@ -573,25 +561,34 @@ public class JingleRtpConnection extends AbstractJingleConnection
final boolean isInitiator = isInitiator(); final boolean isInitiator = isInitiator();
final RtpContentMap currentOutgoing = this.outgoingContentAdd; final RtpContentMap currentOutgoing = this.outgoingContentAdd;
final RtpContentMap remoteContentMap = this.getRemoteContentMap(); final RtpContentMap remoteContentMap = this.getRemoteContentMap();
final Set<String> currentOutgoingMediaIds = currentOutgoing == null ? Collections.emptySet() : currentOutgoing.contents.keySet(); final Set<String> currentOutgoingMediaIds =
currentOutgoing == null
? Collections.emptySet()
: currentOutgoing.contents.keySet();
Log.d(Config.LOGTAG, "receiveContentModification(" + modification + ")"); Log.d(Config.LOGTAG, "receiveContentModification(" + modification + ")");
if (currentOutgoing != null && currentOutgoingMediaIds.containsAll(modification.keySet())) { if (currentOutgoing != null && currentOutgoingMediaIds.containsAll(modification.keySet())) {
respondOk(jinglePacket); respondOk(jinglePacket);
final RtpContentMap modifiedContentMap; final RtpContentMap modifiedContentMap;
try { try {
modifiedContentMap = currentOutgoing.modifiedSendersChecked(isInitiator, modification); modifiedContentMap =
currentOutgoing.modifiedSendersChecked(isInitiator, modification);
} catch (final IllegalArgumentException e) { } catch (final IllegalArgumentException e) {
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
return; return;
} }
this.outgoingContentAdd = modifiedContentMap; this.outgoingContentAdd = modifiedContentMap;
Log.d(Config.LOGTAG, id.account.getJid().asBareJid()+": processed content-modification for pending content-add"); Log.d(
} else if (remoteContentMap != null && remoteContentMap.contents.keySet().containsAll(modification.keySet())) { Config.LOGTAG,
id.account.getJid().asBareJid()
+ ": processed content-modification for pending content-add");
} else if (remoteContentMap != null
&& remoteContentMap.contents.keySet().containsAll(modification.keySet())) {
respondOk(jinglePacket); respondOk(jinglePacket);
final RtpContentMap modifiedRemoteContentMap; final RtpContentMap modifiedRemoteContentMap;
try { try {
modifiedRemoteContentMap = remoteContentMap.modifiedSendersChecked(isInitiator, modification); modifiedRemoteContentMap =
remoteContentMap.modifiedSendersChecked(isInitiator, modification);
} catch (final IllegalArgumentException e) { } catch (final IllegalArgumentException e) {
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
@ -601,20 +598,27 @@ public class JingleRtpConnection extends AbstractJingleConnection
try { try {
offer = SessionDescription.of(modifiedRemoteContentMap, !isInitiator()); offer = SessionDescription.of(modifiedRemoteContentMap, !isInitiator());
} catch (final IllegalArgumentException | NullPointerException e) { } catch (final IllegalArgumentException | NullPointerException e) {
Log.d(Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": unable convert offer from content-modify to SDP", e); Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": unable convert offer from content-modify to SDP",
e);
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
return; return;
} }
Log.d(Config.LOGTAG, id.account.getJid().asBareJid()+": auto accepting content-modification"); Log.d(
Config.LOGTAG,
id.account.getJid().asBareJid() + ": auto accepting content-modification");
this.autoAcceptContentModify(modifiedRemoteContentMap, offer); this.autoAcceptContentModify(modifiedRemoteContentMap, offer);
} else { } else {
Log.d(Config.LOGTAG,"received unsupported content modification "+modification); Log.d(Config.LOGTAG, "received unsupported content modification " + modification);
respondWithItemNotFound(jinglePacket); respondWithItemNotFound(jinglePacket);
} }
} }
private void autoAcceptContentModify(final RtpContentMap modifiedRemoteContentMap, final SessionDescription offer) { private void autoAcceptContentModify(
final RtpContentMap modifiedRemoteContentMap, final SessionDescription offer) {
this.setRemoteContentMap(modifiedRemoteContentMap); this.setRemoteContentMap(modifiedRemoteContentMap);
final org.webrtc.SessionDescription sdp = final org.webrtc.SessionDescription sdp =
new org.webrtc.SessionDescription( new org.webrtc.SessionDescription(
@ -625,8 +629,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
final SessionDescription answer = setLocalSessionDescription(); final SessionDescription answer = setLocalSessionDescription();
final RtpContentMap rtpContentMap = RtpContentMap.of(answer, isInitiator()); final RtpContentMap rtpContentMap = RtpContentMap.of(answer, isInitiator());
modifyLocalContentMap(rtpContentMap); modifyLocalContentMap(rtpContentMap);
// we do not need to send an answer but do we have to resend the candidates currently in SDP? // we do not need to send an answer but do we have to resend the candidates currently in
//resendCandidatesFromSdp(answer); // SDP?
// resendCandidatesFromSdp(answer);
webRTCWrapper.setIsReadyToReceiveIceCandidates(true); webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
} catch (final Exception e) { } catch (final Exception e) {
Log.d(Config.LOGTAG, "unable to accept content add", Throwables.getRootCause(e)); Log.d(Config.LOGTAG, "unable to accept content add", Throwables.getRootCause(e));
@ -635,17 +640,20 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
} }
private static ImmutableMultimap<String, IceUdpTransportInfo.Candidate> parseCandidates(final SessionDescription answer) { private static ImmutableMultimap<String, IceUdpTransportInfo.Candidate> parseCandidates(
final ImmutableMultimap.Builder<String, IceUdpTransportInfo.Candidate> candidateBuilder = new ImmutableMultimap.Builder<>(); final SessionDescription answer) {
for(final SessionDescription.Media media : answer.media) { final ImmutableMultimap.Builder<String, IceUdpTransportInfo.Candidate> candidateBuilder =
new ImmutableMultimap.Builder<>();
for (final SessionDescription.Media media : answer.media) {
final String mid = Iterables.getFirst(media.attributes.get("mid"), null); final String mid = Iterables.getFirst(media.attributes.get("mid"), null);
if (Strings.isNullOrEmpty(mid)) { if (Strings.isNullOrEmpty(mid)) {
continue; continue;
} }
for(final String sdpCandidate : media.attributes.get("candidate")) { for (final String sdpCandidate : media.attributes.get("candidate")) {
final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttributeValue(sdpCandidate, null); final IceUdpTransportInfo.Candidate candidate =
IceUdpTransportInfo.Candidate.fromSdpAttributeValue(sdpCandidate, null);
if (candidate != null) { if (candidate != null) {
candidateBuilder.put(mid,candidate); candidateBuilder.put(mid, candidate);
} }
} }
} }
@ -677,7 +685,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
if (ourSummary.equals(ContentAddition.summary(receivedContentReject))) { if (ourSummary.equals(ContentAddition.summary(receivedContentReject))) {
this.outgoingContentAdd = null; this.outgoingContentAdd = null;
respondOk(jinglePacket); respondOk(jinglePacket);
Log.d(Config.LOGTAG,jinglePacket.toString()); Log.d(Config.LOGTAG, jinglePacket.toString());
receiveContentReject(ourSummary); receiveContentReject(ourSummary);
} else { } else {
Log.d(Config.LOGTAG, "received content-reject did not match our outgoing content-add"); Log.d(Config.LOGTAG, "received content-reject did not match our outgoing content-add");
@ -813,7 +821,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
"Unexpected rollback condition. Senders were not uniformly none"); "Unexpected rollback condition. Senders were not uniformly none");
} }
public synchronized void acceptContentAdd(@NonNull final Set<ContentAddition.Summary> contentAddition) { public synchronized void acceptContentAdd(
@NonNull final Set<ContentAddition.Summary> contentAddition) {
final RtpContentMap incomingContentAdd = this.incomingContentAdd; final RtpContentMap incomingContentAdd = this.incomingContentAdd;
if (incomingContentAdd == null) { if (incomingContentAdd == null) {
throw new IllegalStateException("No incoming content add"); throw new IllegalStateException("No incoming content add");
@ -822,37 +831,61 @@ public class JingleRtpConnection extends AbstractJingleConnection
if (contentAddition.equals(ContentAddition.summary(incomingContentAdd))) { if (contentAddition.equals(ContentAddition.summary(incomingContentAdd))) {
this.incomingContentAdd = null; this.incomingContentAdd = null;
final Set<Content.Senders> senders = incomingContentAdd.getSenders(); final Set<Content.Senders> senders = incomingContentAdd.getSenders();
Log.d(Config.LOGTAG,"senders of incoming content-add: "+senders); Log.d(Config.LOGTAG, "senders of incoming content-add: " + senders);
if (senders.equals(Content.Senders.receiveOnly(isInitiator()))) { if (senders.equals(Content.Senders.receiveOnly(isInitiator()))) {
Log.d(Config.LOGTAG,"content addition is receive only. we want to upgrade to 'both'"); Log.d(
final RtpContentMap modifiedSenders = incomingContentAdd.modifiedSenders(Content.Senders.BOTH); Config.LOGTAG,
final JinglePacket proposedContentModification = modifiedSenders.toStub().toJinglePacket(JinglePacket.Action.CONTENT_MODIFY, id.sessionId); "content addition is receive only. we want to upgrade to 'both'");
final RtpContentMap modifiedSenders =
incomingContentAdd.modifiedSenders(Content.Senders.BOTH);
final JinglePacket proposedContentModification =
modifiedSenders
.toStub()
.toJinglePacket(JinglePacket.Action.CONTENT_MODIFY, id.sessionId);
proposedContentModification.setTo(id.with); proposedContentModification.setTo(id.with);
xmppConnectionService.sendIqPacket(id.account, proposedContentModification, (account, response) -> { xmppConnectionService.sendIqPacket(
id.account,
proposedContentModification,
(account, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) { if (response.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": remote has accepted our upgrade to senders=both"); Log.d(
acceptContentAdd(ContentAddition.summary(modifiedSenders), modifiedSenders); Config.LOGTAG,
id.account.getJid().asBareJid()
+ ": remote has accepted our upgrade to senders=both");
acceptContentAdd(
ContentAddition.summary(modifiedSenders), modifiedSenders);
} else { } else {
Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": remote has rejected our upgrade to senders=both"); Log.d(
Config.LOGTAG,
id.account.getJid().asBareJid()
+ ": remote has rejected our upgrade to senders=both");
acceptContentAdd(contentAddition, incomingContentAdd); acceptContentAdd(contentAddition, incomingContentAdd);
} }
}); });
} }
} else { } else {
throw new IllegalStateException("Accepted content add does not match pending content-add"); throw new IllegalStateException(
"Accepted content add does not match pending content-add");
} }
} }
private void acceptContentAdd(@NonNull final Set<ContentAddition.Summary> contentAddition, final RtpContentMap incomingContentAdd) { private void acceptContentAdd(
@NonNull final Set<ContentAddition.Summary> contentAddition,
final RtpContentMap incomingContentAdd) {
final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup();
final RtpContentMap modifiedContentMap = getRemoteContentMap().addContent(incomingContentAdd, setup); final RtpContentMap modifiedContentMap =
getRemoteContentMap().addContent(incomingContentAdd, setup);
this.setRemoteContentMap(modifiedContentMap); this.setRemoteContentMap(modifiedContentMap);
final SessionDescription offer; final SessionDescription offer;
try { try {
offer = SessionDescription.of(modifiedContentMap, !isInitiator()); offer = SessionDescription.of(modifiedContentMap, !isInitiator());
} catch (final IllegalArgumentException | NullPointerException e) { } catch (final IllegalArgumentException | NullPointerException e) {
Log.d(Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": unable convert offer from content-add to SDP", e); Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": unable convert offer from content-add to SDP",
e);
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
return; return;
@ -893,10 +926,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
addIceCandidatesFromBlackLog(); addIceCandidatesFromBlackLog();
modifyLocalContentMap(rtpContentMap); modifyLocalContentMap(rtpContentMap);
final ListenableFuture<RtpContentMap> future = prepareOutgoingContentMap(contentAcceptMap); final ListenableFuture<RtpContentMap> future =
prepareOutgoingContentMap(contentAcceptMap);
Futures.addCallback( Futures.addCallback(
future, future,
new FutureCallback<RtpContentMap>() { new FutureCallback<>() {
@Override @Override
public void onSuccess(final RtpContentMap rtpContentMap) { public void onSuccess(final RtpContentMap rtpContentMap) {
sendContentAccept(rtpContentMap); sendContentAccept(rtpContentMap);
@ -917,7 +951,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
private void sendContentAccept(final RtpContentMap contentAcceptMap) { private void sendContentAccept(final RtpContentMap contentAcceptMap) {
final JinglePacket jinglePacket = contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId); final JinglePacket jinglePacket =
contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId);
send(jinglePacket); send(jinglePacket);
} }
@ -963,7 +998,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
// ICE-restart // ICE-restart
// and if that's the case we are seeing an answer. // and if that's the case we are seeing an answer.
// This might be more spec compliant but also more error prone potentially // This might be more spec compliant but also more error prone potentially
final boolean isSignalStateStable = this.webRTCWrapper.getSignalingState() == PeerConnection.SignalingState.STABLE; final boolean isSignalStateStable =
this.webRTCWrapper.getSignalingState() == PeerConnection.SignalingState.STABLE;
// TODO a stable signal state can be another indicator that we have an offer to restart ICE // TODO a stable signal state can be another indicator that we have an offer to restart ICE
final boolean isOffer = rtpContentMap.emptyCandidates(); final boolean isOffer = rtpContentMap.emptyCandidates();
final RtpContentMap restartContentMap; final RtpContentMap restartContentMap;
@ -1027,7 +1063,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
final RtpContentMap restartContentMap, final RtpContentMap restartContentMap,
final boolean isOffer) final boolean isOffer)
throws ExecutionException, InterruptedException { throws ExecutionException, InterruptedException {
final SessionDescription sessionDescription = SessionDescription.of(restartContentMap, !isInitiator()); final SessionDescription sessionDescription =
SessionDescription.of(restartContentMap, !isInitiator());
final org.webrtc.SessionDescription.Type type = final org.webrtc.SessionDescription.Type type =
isOffer isOffer
? org.webrtc.SessionDescription.Type.OFFER ? org.webrtc.SessionDescription.Type.OFFER
@ -1127,7 +1164,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);
} }
} }
private ListenableFuture<RtpContentMap> receiveRtpContentMap(final RtpContentMap receivedContentMap, final boolean expectVerification) {
private ListenableFuture<RtpContentMap> receiveRtpContentMap(
final RtpContentMap receivedContentMap, final boolean expectVerification) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
"receiveRtpContentMap(" "receiveRtpContentMap("
@ -1183,7 +1222,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false); final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
Futures.addCallback( Futures.addCallback(
future, future,
new FutureCallback<RtpContentMap>() { new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable RtpContentMap rtpContentMap) { public void onSuccess(@Nullable RtpContentMap rtpContentMap) {
receiveSessionInitiate(jinglePacket, rtpContentMap); receiveSessionInitiate(jinglePacket, rtpContentMap);
@ -1272,7 +1311,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint());
Futures.addCallback( Futures.addCallback(
future, future,
new FutureCallback<RtpContentMap>() { new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable RtpContentMap rtpContentMap) { public void onSuccess(@Nullable RtpContentMap rtpContentMap) {
receiveSessionAccept(jinglePacket, rtpContentMap); receiveSessionAccept(jinglePacket, rtpContentMap);
@ -1438,7 +1477,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
} }
private void failureToPerformAction(final JinglePacket.Action action, final Throwable throwable) { private void failureToPerformAction(
final JinglePacket.Action action, final Throwable throwable) {
if (isTerminated()) { if (isTerminated()) {
return; return;
} }
@ -1459,7 +1499,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
private void prepareSessionAccept( private void prepareSessionAccept(
final org.webrtc.SessionDescription webRTCSessionDescription, final boolean includeCandidates) { final org.webrtc.SessionDescription webRTCSessionDescription,
final boolean includeCandidates) {
final SessionDescription sessionDescription = final SessionDescription sessionDescription =
SessionDescription.parse(webRTCSessionDescription.description); SessionDescription.parse(webRTCSessionDescription.description);
final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false);
@ -1475,7 +1516,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
prepareOutgoingContentMap(respondingRtpContentMap); prepareOutgoingContentMap(respondingRtpContentMap);
Futures.addCallback( Futures.addCallback(
outgoingContentMapFuture, outgoingContentMapFuture,
new FutureCallback<RtpContentMap>() { new FutureCallback<>() {
@Override @Override
public void onSuccess(final RtpContentMap outgoingContentMap) { public void onSuccess(final RtpContentMap outgoingContentMap) {
if (includeCandidates) { if (includeCandidates) {
@ -1548,30 +1589,23 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ ": delivered message to JingleRtpConnection " + ": delivered message to JingleRtpConnection "
+ message); + message);
switch (message.getName()) { switch (message.getName()) {
case "propose": case "propose" -> receivePropose(
receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp); from, Propose.upgrade(message), serverMessageId, timestamp);
break; case "proceed" -> receiveProceed(
case "proceed": from, Proceed.upgrade(message), serverMessageId, timestamp);
receiveProceed(from, Proceed.upgrade(message), serverMessageId, timestamp); case "retract" -> receiveRetract(from, serverMessageId, timestamp);
break; case "reject" -> receiveReject(from, serverMessageId, timestamp);
case "retract": case "accept" -> receiveAccept(from, serverMessageId, timestamp);
receiveRetract(from, serverMessageId, timestamp);
break;
case "reject":
receiveReject(from, serverMessageId, timestamp);
break;
case "accept":
receiveAccept(from, serverMessageId, timestamp);
break;
default:
break;
} }
} }
void deliverFailedProceed(final String message) { void deliverFailedProceed(final String message) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
id.account.getJid().asBareJid() + ": receive message error for proceed message ("+Strings.nullToEmpty(message)+")"); id.account.getJid().asBareJid()
+ ": receive message error for proceed message ("
+ Strings.nullToEmpty(message)
+ ")");
if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) { if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) {
webRTCWrapper.close(); webRTCWrapper.close();
Log.d( Log.d(
@ -1609,9 +1643,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
this.message.setTime(timestamp); this.message.setTime(timestamp);
this.message.setCarbon(true); // indicate that call was accepted on other device this.message.setCarbon(true); // indicate that call was accepted on other device
this.writeLogMessageSuccess(0); this.writeLogMessageSuccess(0);
this.xmppConnectionService this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
.getNotificationService()
.cancelIncomingCallNotification();
this.finish(); this.finish();
} }
@ -1749,14 +1781,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
private synchronized void ringingTimeout() { private synchronized void ringingTimeout() {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": timeout reached for ringing"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": timeout reached for ringing");
switch (this.state) { switch (this.state) {
case PROPOSED: case PROPOSED -> {
message.markUnread(); message.markUnread();
rejectCallFromProposed(); rejectCallFromProposed();
break; }
case SESSION_INITIALIZED: case SESSION_INITIALIZED -> {
message.markUnread(); message.markUnread();
rejectCallFromSessionInitiate(); rejectCallFromSessionInitiate();
break; }
} }
xmppConnectionService.getNotificationService().pushMissedCallNow(message); xmppConnectionService.getNotificationService().pushMissedCallNow(message);
} }
@ -1932,7 +1964,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
private void prepareSessionInitiate( private void prepareSessionInitiate(
final org.webrtc.SessionDescription webRTCSessionDescription, final boolean includeCandidates, final State targetState) { final org.webrtc.SessionDescription webRTCSessionDescription,
final boolean includeCandidates,
final State targetState) {
final SessionDescription sessionDescription = final SessionDescription sessionDescription =
SessionDescription.parse(webRTCSessionDescription.description); SessionDescription.parse(webRTCSessionDescription.description);
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true);
@ -1947,7 +1981,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
encryptSessionInitiate(rtpContentMap); encryptSessionInitiate(rtpContentMap);
Futures.addCallback( Futures.addCallback(
outgoingContentMapFuture, outgoingContentMapFuture,
new FutureCallback<RtpContentMap>() { new FutureCallback<>() {
@Override @Override
public void onSuccess(final RtpContentMap outgoingContentMap) { public void onSuccess(final RtpContentMap outgoingContentMap) {
if (includeCandidates) { if (includeCandidates) {
@ -1956,7 +1990,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
"including " "including "
+ candidates.size() + candidates.size()
+ " candidates in session initiate"); + " candidates in session initiate");
sendSessionInitiate(outgoingContentMap.withCandidates(candidates), targetState); sendSessionInitiate(
outgoingContentMap.withCandidates(candidates), targetState);
webRTCWrapper.resetPendingCandidates(); webRTCWrapper.resetPendingCandidates();
} else { } else {
sendSessionInitiate(outgoingContentMap, targetState); sendSessionInitiate(outgoingContentMap, targetState);
@ -2170,60 +2205,63 @@ public class JingleRtpConnection extends AbstractJingleConnection
public RtpEndUserState getEndUserState() { public RtpEndUserState getEndUserState() {
switch (this.state) { switch (this.state) {
case NULL: case NULL, PROPOSED, SESSION_INITIALIZED -> {
case PROPOSED:
case SESSION_INITIALIZED:
if (isInitiator()) { if (isInitiator()) {
return RtpEndUserState.RINGING; return RtpEndUserState.RINGING;
} else { } else {
return RtpEndUserState.INCOMING_CALL; return RtpEndUserState.INCOMING_CALL;
} }
case PROCEED: }
case PROCEED -> {
if (isInitiator()) { if (isInitiator()) {
return RtpEndUserState.RINGING; return RtpEndUserState.RINGING;
} else { } else {
return RtpEndUserState.ACCEPTING_CALL; return RtpEndUserState.ACCEPTING_CALL;
} }
case SESSION_INITIALIZED_PRE_APPROVED: }
case SESSION_INITIALIZED_PRE_APPROVED -> {
if (isInitiator()) { if (isInitiator()) {
return RtpEndUserState.RINGING; return RtpEndUserState.RINGING;
} else { } else {
return RtpEndUserState.CONNECTING; return RtpEndUserState.CONNECTING;
} }
case SESSION_ACCEPTED: }
case SESSION_ACCEPTED -> {
final ContentAddition ca = getPendingContentAddition(); final ContentAddition ca = getPendingContentAddition();
if (ca != null && ca.direction == ContentAddition.Direction.INCOMING) { if (ca != null && ca.direction == ContentAddition.Direction.INCOMING) {
return RtpEndUserState.INCOMING_CONTENT_ADD; return RtpEndUserState.INCOMING_CONTENT_ADD;
} }
return getPeerConnectionStateAsEndUserState(); return getPeerConnectionStateAsEndUserState();
case REJECTED: }
case REJECTED_RACED: case REJECTED, REJECTED_RACED, TERMINATED_DECLINED_OR_BUSY -> {
case TERMINATED_DECLINED_OR_BUSY:
if (isInitiator()) { if (isInitiator()) {
return RtpEndUserState.DECLINED_OR_BUSY; return RtpEndUserState.DECLINED_OR_BUSY;
} else { } else {
return RtpEndUserState.ENDED; return RtpEndUserState.ENDED;
} }
case TERMINATED_SUCCESS: }
case ACCEPTED: case TERMINATED_SUCCESS, ACCEPTED, RETRACTED, TERMINATED_CANCEL_OR_TIMEOUT -> {
case RETRACTED:
case TERMINATED_CANCEL_OR_TIMEOUT:
return RtpEndUserState.ENDED; return RtpEndUserState.ENDED;
case RETRACTED_RACED: }
case RETRACTED_RACED -> {
if (isInitiator()) { if (isInitiator()) {
return RtpEndUserState.ENDED; return RtpEndUserState.ENDED;
} else { } else {
return RtpEndUserState.RETRACTED; return RtpEndUserState.RETRACTED;
} }
case TERMINATED_CONNECTIVITY_ERROR: }
case TERMINATED_CONNECTIVITY_ERROR -> {
return zeroDuration() return zeroDuration()
? RtpEndUserState.CONNECTIVITY_ERROR ? RtpEndUserState.CONNECTIVITY_ERROR
: RtpEndUserState.CONNECTIVITY_LOST_ERROR; : RtpEndUserState.CONNECTIVITY_LOST_ERROR;
case TERMINATED_APPLICATION_FAILURE: }
case TERMINATED_APPLICATION_FAILURE -> {
return RtpEndUserState.APPLICATION_ERROR; return RtpEndUserState.APPLICATION_ERROR;
case TERMINATED_SECURITY_ERROR: }
case TERMINATED_SECURITY_ERROR -> {
return RtpEndUserState.SECURITY_ERROR; return RtpEndUserState.SECURITY_ERROR;
} }
}
throw new IllegalStateException( throw new IllegalStateException(
String.format("%s has no equivalent EndUserState", this.state)); String.format("%s has no equivalent EndUserState", this.state));
} }
@ -2237,19 +2275,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
// be in SESSION_ACCEPTED even though the peerConnection has been torn down // be in SESSION_ACCEPTED even though the peerConnection has been torn down
return RtpEndUserState.ENDING_CALL; return RtpEndUserState.ENDING_CALL;
} }
switch (state) { return switch (state) {
case CONNECTED: case CONNECTED -> RtpEndUserState.CONNECTED;
return RtpEndUserState.CONNECTED; case NEW, CONNECTING -> RtpEndUserState.CONNECTING;
case NEW: case CLOSED -> RtpEndUserState.ENDING_CALL;
case CONNECTING: default -> zeroDuration()
return RtpEndUserState.CONNECTING;
case CLOSED:
return RtpEndUserState.ENDING_CALL;
default:
return zeroDuration()
? RtpEndUserState.CONNECTIVITY_ERROR ? RtpEndUserState.CONNECTIVITY_ERROR
: RtpEndUserState.RECONNECTING; : RtpEndUserState.RECONNECTING;
} };
} }
public ContentAddition getPendingContentAddition() { public ContentAddition getPendingContentAddition() {
@ -2284,9 +2317,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
} else if (initiatorContentMap != null) { } else if (initiatorContentMap != null) {
return initiatorContentMap.getMedia(); return initiatorContentMap.getMedia();
} else if (isTerminated()) { } else if (isTerminated()) {
return Collections.emptySet(); //we might fail before we ever got a chance to set media return Collections.emptySet(); // we might fail before we ever got a chance to set media
} else { } else {
return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); return Preconditions.checkNotNull(
this.proposedMedia, "RTP connection has not been initialized properly");
} }
} }
@ -2306,35 +2340,29 @@ public class JingleRtpConnection extends AbstractJingleConnection
throw new IllegalStateException(String.format("%s has already been proposed", media)); throw new IllegalStateException(String.format("%s has already been proposed", media));
} }
// TODO add state protection - can only add while ACCEPTED or so // TODO add state protection - can only add while ACCEPTED or so
Log.d(Config.LOGTAG,"adding media: "+media); Log.d(Config.LOGTAG, "adding media: " + media);
return webRTCWrapper.addTrack(media); return webRTCWrapper.addTrack(media);
} }
public synchronized void acceptCall() { public synchronized void acceptCall() {
switch (this.state) { switch (this.state) {
case PROPOSED: case PROPOSED -> {
cancelRingingTimeout(); cancelRingingTimeout();
acceptCallFromProposed(); acceptCallFromProposed();
break; }
case SESSION_INITIALIZED: case SESSION_INITIALIZED -> {
cancelRingingTimeout(); cancelRingingTimeout();
acceptCallFromSessionInitialized(); acceptCallFromSessionInitialized();
break; }
case ACCEPTED: case ACCEPTED -> Log.w(
Log.w(
Config.LOGTAG, Config.LOGTAG,
id.account.getJid().asBareJid() id.account.getJid().asBareJid()
+ ": the call has already been accepted with another client. UI was just lagging behind"); + ": the call has already been accepted with another client. UI was just lagging behind");
break; case PROCEED, SESSION_ACCEPTED -> Log.w(
case PROCEED:
case SESSION_ACCEPTED:
Log.w(
Config.LOGTAG, Config.LOGTAG,
id.account.getJid().asBareJid() id.account.getJid().asBareJid()
+ ": the call has already been accepted. user probably double tapped the UI"); + ": the call has already been accepted. user probably double tapped the UI");
break; default -> throw new IllegalStateException("Can not accept call from " + this.state);
default:
throw new IllegalStateException("Can not accept call from " + this.state);
} }
} }
@ -2356,14 +2384,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
return; return;
} }
switch (this.state) { switch (this.state) {
case PROPOSED: case PROPOSED -> rejectCallFromProposed();
rejectCallFromProposed(); case SESSION_INITIALIZED -> rejectCallFromSessionInitiate();
break; default -> throw new IllegalStateException("Can not reject call from " + this.state);
case SESSION_INITIALIZED:
rejectCallFromSessionInitiate();
break;
default:
throw new IllegalStateException("Can not reject call from " + this.state);
} }
} }
@ -2428,9 +2451,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
finish(); finish();
} }
private void setupWebRTC(final Set<Media> media, final List<PeerConnection.IceServer> iceServers, final boolean trickle) throws WebRTCWrapper.InitializationException { private void setupWebRTC(
final Set<Media> media,
final List<PeerConnection.IceServer> iceServers,
final boolean trickle)
throws WebRTCWrapper.InitializationException {
this.jingleConnectionManager.ensureConnectionIsRegistered(this); this.jingleConnectionManager.ensureConnectionIsRegistered(this);
this.webRTCWrapper.setup(this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media)); this.webRTCWrapper.setup(
this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media));
this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle); this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle);
} }
@ -2606,7 +2634,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
final Throwable cause = Throwables.getRootCause(e); final Throwable cause = Throwables.getRootCause(e);
webRTCWrapper.close(); webRTCWrapper.close();
if (isTerminated()) { if (isTerminated()) {
Log.d(Config.LOGTAG, "failed to renegotiate. session was already terminated", cause); Log.d(
Config.LOGTAG,
"failed to renegotiate. session was already terminated",
cause);
return; return;
} }
Log.d(Config.LOGTAG, "failed to renegotiate. sending session-terminate", cause); Log.d(Config.LOGTAG, "failed to renegotiate. sending session-terminate", cause);
@ -2691,7 +2722,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
prepareOutgoingContentMap(contentAdd); prepareOutgoingContentMap(contentAdd);
Futures.addCallback( Futures.addCallback(
outgoingContentMapFuture, outgoingContentMapFuture,
new FutureCallback<RtpContentMap>() { new FutureCallback<>() {
@Override @Override
public void onSuccess(final RtpContentMap outgoingContentMap) { public void onSuccess(final RtpContentMap outgoingContentMap) {
sendContentAdd(outgoingContentMap); sendContentAdd(outgoingContentMap);
@ -2889,9 +2920,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
continue; continue;
} }
if (Arrays.asList("stun", "stuns", "turn", "turns") if (Arrays.asList("stun", "stuns", "turn", "turns")
.contains(type) .contains(type)
&& Arrays.asList("udp", "tcp").contains(transport)) { && Arrays.asList("udp", "tcp").contains(transport)) {
@ -2906,10 +2934,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
// STUN URLs do not support a query section since M110 // STUN URLs do not support a query section since M110
final String uri; final String uri;
if (Arrays.asList("stun","stuns").contains(type)) { if (Arrays.asList("stun", "stuns").contains(type)) {
uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host),port); uri =
String.format(
"%s:%s:%s",
type, IP.wrapIPv6(host), port);
} else { } else {
uri = String.format( uri =
String.format(
"%s:%s:%s?transport=%s", "%s:%s:%s?transport=%s",
type, type,
IP.wrapIPv6(host), IP.wrapIPv6(host),