proper iq tracing (handling of errors); responding to all iqs

This commit is contained in:
Daniel Gultsch 2020-04-09 15:22:03 +02:00
parent 15a2491d7b
commit 268eedad89
9 changed files with 174 additions and 37 deletions

View file

@ -169,6 +169,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
case CONNECTIVITY_ERROR: case CONNECTIVITY_ERROR:
binding.status.setText(R.string.rtp_state_connectivity_error); binding.status.setText(R.string.rtp_state_connectivity_error);
break; break;
case APPLICATION_ERROR:
binding.status.setText(R.string.rtp_state_application_failure);
break;
} }
} }
@ -191,7 +194,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp); this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp);
this.binding.endCall.show(); this.binding.endCall.show();
this.binding.acceptCall.hide(); this.binding.acceptCall.hide();
} else if (state == RtpEndUserState.CONNECTIVITY_ERROR) { } else if (state == RtpEndUserState.CONNECTIVITY_ERROR || state == RtpEndUserState.APPLICATION_ERROR) {
this.binding.rejectCall.setOnClickListener(this::exit); this.binding.rejectCall.setOnClickListener(this::exit);
this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
this.binding.rejectCall.show(); this.binding.rejectCall.show();

View file

@ -95,6 +95,7 @@ public abstract class AbstractJingleConnection {
TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close
TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call) TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call)
TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button) TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button)
TERMINATED_CANCEL_OR_TIMEOUT //more or less the same as retracted; caller pressed end call before session was accepted TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted
TERMINATED_APPLICATION_FAILURE
} }
} }

View file

@ -55,22 +55,27 @@ public class JingleConnectionManager extends AbstractConnectionManager {
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)) { } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)) {
connection = new JingleRtpConnection(this, id, from); connection = new JingleRtpConnection(this, id, from);
} else { } else {
//TODO return feature-not-implemented respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel");
return; return;
} }
connections.put(id, connection); connections.put(id, connection);
connection.deliverPacket(packet); connection.deliverPacket(packet);
} else { } else {
Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet);
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
final Element error = response.addChild("error");
error.setAttribute("type", "cancel");
error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
error.addChild("unknown-session", "urn:xmpp:jingle:errors:1");
account.getXmppConnection().sendIqPacket(response, null);
} }
} }
public void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) {
final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error");
error.setAttribute("type", conditionType);
error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1");
account.getXmppConnection().sendIqPacket(response, null);
}
public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message) { public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message) {
Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace())); Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace()));
final String sessionId = message.getAttribute("id"); final String sessionId = message.getAttribute("id");

View file

@ -34,12 +34,40 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
static { static {
final ImmutableMap.Builder<State, Collection<State>> transitionBuilder = new ImmutableMap.Builder<>(); final ImmutableMap.Builder<State, Collection<State>> transitionBuilder = new ImmutableMap.Builder<>();
transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED)); transitionBuilder.put(State.NULL, ImmutableList.of(
transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED, State.RETRACTED)); State.PROPOSED,
transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED_PRE_APPROVED, State.TERMINATED_SUCCESS)); State.SESSION_INITIALIZED,
transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY)); State.TERMINATED_APPLICATION_FAILURE
transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY)); ));
transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of(State.TERMINATED_SUCCESS, State.TERMINATED_CONNECTIVITY_ERROR)); transitionBuilder.put(State.PROPOSED, ImmutableList.of(
State.ACCEPTED,
State.PROCEED,
State.REJECTED,
State.RETRACTED,
State.TERMINATED_APPLICATION_FAILURE
));
transitionBuilder.put(State.PROCEED, ImmutableList.of(
State.SESSION_INITIALIZED_PRE_APPROVED,
State.TERMINATED_SUCCESS,
State.TERMINATED_APPLICATION_FAILURE
));
transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(
State.SESSION_ACCEPTED,
State.TERMINATED_CANCEL_OR_TIMEOUT,
State.TERMINATED_DECLINED_OR_BUSY,
State.TERMINATED_APPLICATION_FAILURE
));
transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of(
State.SESSION_ACCEPTED,
State.TERMINATED_CANCEL_OR_TIMEOUT,
State.TERMINATED_DECLINED_OR_BUSY,
State.TERMINATED_APPLICATION_FAILURE
));
transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of(
State.TERMINATED_SUCCESS,
State.TERMINATED_CONNECTIVITY_ERROR,
State.TERMINATED_APPLICATION_FAILURE
));
VALID_TRANSITIONS = transitionBuilder.build(); VALID_TRANSITIONS = transitionBuilder.build();
} }
@ -64,6 +92,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
case CANCEL: case CANCEL:
case TIMEOUT: case TIMEOUT:
return State.TERMINATED_CANCEL_OR_TIMEOUT; return State.TERMINATED_CANCEL_OR_TIMEOUT;
case FAILED_APPLICATION:
return State.TERMINATED_APPLICATION_FAILURE;
default: default:
return State.TERMINATED_CONNECTIVITY_ERROR; return State.TERMINATED_CONNECTIVITY_ERROR;
} }
@ -86,12 +116,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
receiveSessionTerminate(jinglePacket); receiveSessionTerminate(jinglePacket);
break; break;
default: default:
respondOk(jinglePacket);
Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction())); Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction()));
break; break;
} }
} }
private void receiveSessionTerminate(final JinglePacket jinglePacket) { private void receiveSessionTerminate(final JinglePacket jinglePacket) {
respondOk(jinglePacket);
final Reason reason = jinglePacket.getReason(); final Reason reason = jinglePacket.getReason();
final State previous = this.state; final State previous = this.state;
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + reason + " while in state " + previous); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + reason + " while in state " + previous);
@ -105,11 +137,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void receiveTransportInfo(final JinglePacket jinglePacket) { private void receiveTransportInfo(final JinglePacket jinglePacket) {
if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
respondOk(jinglePacket);
final RtpContentMap contentMap; final RtpContentMap contentMap;
try { try {
contentMap = RtpContentMap.of(jinglePacket); contentMap = RtpContentMap.of(jinglePacket);
} catch (IllegalArgumentException | NullPointerException e) { } catch (IllegalArgumentException | NullPointerException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e);
return; return;
} }
final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap;
@ -136,13 +169,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
} else { } else {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state);
respondWithOutOfOrder(jinglePacket);
} }
} }
private void receiveSessionInitiate(final JinglePacket jinglePacket) { private void receiveSessionInitiate(final JinglePacket jinglePacket) {
if (isInitiator()) { if (isInitiator()) {
Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid()));
//TODO respond with out-of-order respondWithOutOfOrder(jinglePacket);
return; return;
} }
final RtpContentMap contentMap; final RtpContentMap contentMap;
@ -150,6 +184,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
contentMap = RtpContentMap.of(jinglePacket); contentMap = RtpContentMap.of(jinglePacket);
contentMap.requireContentDescriptions(); contentMap.requireContentDescriptions();
} catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) {
respondOk(jinglePacket);
sendSessionTerminate(Reason.FAILED_APPLICATION);
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e);
return; return;
} }
@ -161,6 +197,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
target = State.SESSION_INITIALIZED; target = State.SESSION_INITIALIZED;
} }
if (transition(target)) { if (transition(target)) {
respondOk(jinglePacket);
this.initiatorRtpContentMap = contentMap; this.initiatorRtpContentMap = contentMap;
if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
@ -171,13 +208,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
} else { } else {
Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state)); Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state));
respondWithOutOfOrder(jinglePacket);
} }
} }
private void receiveSessionAccept(final JinglePacket jinglePacket) { private void receiveSessionAccept(final JinglePacket jinglePacket) {
if (!isInitiator()) { if (!isInitiator()) {
Log.d(Config.LOGTAG, String.format("%s: received session-accept even though we were responding", id.account.getJid().asBareJid())); Log.d(Config.LOGTAG, String.format("%s: received session-accept even though we were responding", id.account.getJid().asBareJid()));
//TODO respond with out-of-order respondWithOutOfOrder(jinglePacket);
return; return;
} }
final RtpContentMap contentMap; final RtpContentMap contentMap;
@ -185,28 +223,43 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
contentMap = RtpContentMap.of(jinglePacket); contentMap = RtpContentMap.of(jinglePacket);
contentMap.requireContentDescriptions(); contentMap.requireContentDescriptions();
} catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); respondOk(jinglePacket);
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION);
return; return;
} }
Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents"); Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents");
if (transition(State.SESSION_ACCEPTED)) { if (transition(State.SESSION_ACCEPTED)) {
respondOk(jinglePacket);
receiveSessionAccept(contentMap); receiveSessionAccept(contentMap);
} else { } else {
Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state)); Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state));
//TODO out-of-order respondOk(jinglePacket);
} }
} }
private void receiveSessionAccept(final RtpContentMap contentMap) { private void receiveSessionAccept(final RtpContentMap contentMap) {
this.responderRtpContentMap = contentMap; this.responderRtpContentMap = contentMap;
final SessionDescription sessionDescription;
try {
sessionDescription = SessionDescription.of(contentMap);
} catch (IllegalArgumentException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-accept to SDP", e);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION);
return;
}
org.webrtc.SessionDescription answer = new org.webrtc.SessionDescription( org.webrtc.SessionDescription answer = new org.webrtc.SessionDescription(
org.webrtc.SessionDescription.Type.ANSWER, org.webrtc.SessionDescription.Type.ANSWER,
SessionDescription.of(contentMap).toString() sessionDescription.toString()
); );
try { try {
this.webRTCWrapper.setRemoteDescription(answer).get(); this.webRTCWrapper.setRemoteDescription(answer).get();
} catch (Exception e) { } catch (Exception e) {
Log.d(Config.LOGTAG, "unable to receive session accept", e); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", e);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION);
} }
} }
@ -219,8 +272,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
try { try {
offer = SessionDescription.of(rtpContentMap); offer = SessionDescription.of(rtpContentMap);
} catch (final IllegalArgumentException e) { } catch (final IllegalArgumentException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to process offer", e); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-initiate to SDP", e);
//TODO terminate session with application error webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION);
;
return; return;
} }
sendSessionAccept(offer); sendSessionAccept(offer);
@ -228,7 +283,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void sendSessionAccept(SessionDescription offer) { private void sendSessionAccept(SessionDescription offer) {
discoverIceServers(iceServers -> { discoverIceServers(iceServers -> {
try {
setupWebRTC(iceServers); setupWebRTC(iceServers);
} catch (WebRTCWrapper.InitializationException e) {
sendSessionTerminate(Reason.FAILED_APPLICATION);
return;
}
final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription( final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(
org.webrtc.SessionDescription.Type.OFFER, org.webrtc.SessionDescription.Type.OFFER,
offer.toString() offer.toString()
@ -351,7 +411,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
this.jingleConnectionManager.finishConnection(this); this.jingleConnectionManager.finishConnection(this);
} }
} else { } else {
//TODO a carbon copied proceed from another client of mine has the same logic as `accept`
Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with)); Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with));
} }
} }
@ -374,7 +433,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void sendSessionInitiate(final State targetState) { private void sendSessionInitiate(final State targetState) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
discoverIceServers(iceServers -> { discoverIceServers(iceServers -> {
try {
setupWebRTC(iceServers); setupWebRTC(iceServers);
} catch (WebRTCWrapper.InitializationException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize webrtc");
transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
return;
}
try { try {
org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get();
final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
@ -382,8 +447,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
sendSessionInitiate(rtpContentMap, targetState); sendSessionInitiate(rtpContentMap, targetState);
this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
} catch (Exception e) { } catch (final Exception e) {
Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", e);
webRTCWrapper.close();
if (isInState(targetState)) {
sendSessionTerminate(Reason.FAILED_APPLICATION);
} else {
transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
}
} }
}); });
} }
@ -422,8 +493,43 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void send(final JinglePacket jinglePacket) { private void send(final JinglePacket jinglePacket) {
jinglePacket.setTo(id.with); jinglePacket.setTo(id.with);
//TODO track errors xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> {
xmppConnectionService.sendIqPacket(id.account, jinglePacket, null); if (response.getType() == IqPacket.TYPE.ERROR) {
final String errorCondition = response.getErrorCondition();
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition);
this.webRTCWrapper.close();
final State target;
if (Arrays.asList(
"service-unavailable",
"recipient-unavailable",
"remote-server-not-found",
"remote-server-timeout"
).contains(errorCondition)) {
target = State.TERMINATED_CONNECTIVITY_ERROR;
} else {
target = State.TERMINATED_APPLICATION_FAILURE;
}
if (transition(target)) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminated session with " + id.with);
} else {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not transitioning because already at state=" + this.state);
}
} else if (response.getType() == IqPacket.TYPE.TIMEOUT) {
this.webRTCWrapper.close();
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error");
transition(State.TERMINATED_CONNECTIVITY_ERROR);
this.jingleConnectionManager.finishConnection(this);
}
});
}
private void respondWithOutOfOrder(final JinglePacket jinglePacket) {
jingleConnectionManager.respondWithJingleError(id.account, jinglePacket, "out-of-order", "unexpected-request", "wait");
}
private void respondOk(final JinglePacket jinglePacket) {
xmppConnectionService.sendIqPacket(id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
} }
public RtpEndUserState getEndUserState() { public RtpEndUserState getEndUserState() {
@ -474,6 +580,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return RtpEndUserState.ENDED; return RtpEndUserState.ENDED;
case TERMINATED_CONNECTIVITY_ERROR: case TERMINATED_CONNECTIVITY_ERROR:
return RtpEndUserState.CONNECTIVITY_ERROR; return RtpEndUserState.CONNECTIVITY_ERROR;
case TERMINATED_APPLICATION_FAILURE:
return RtpEndUserState.APPLICATION_ERROR;
} }
throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state));
} }
@ -526,7 +634,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
throw new IllegalStateException("called 'endCall' while in state " + this.state); throw new IllegalStateException("called 'endCall' while in state " + this.state);
} }
private void setupWebRTC(final List<PeerConnection.IceServer> iceServers) { private void setupWebRTC(final List<PeerConnection.IceServer> iceServers) throws WebRTCWrapper.InitializationException {
this.webRTCWrapper.setup(this.xmppConnectionService); this.webRTCWrapper.setup(this.xmppConnectionService);
this.webRTCWrapper.initializePeerConnection(iceServers); this.webRTCWrapper.initializePeerConnection(iceServers);
} }

View file

@ -6,10 +6,10 @@ public enum RtpEndUserState {
CONNECTED, //session-accepted and webrtc peer connection is connected CONNECTED, //session-accepted and webrtc peer connection is connected
FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
RINGING, //'propose' has been sent out and it has been 184 acked RINGING, //'propose' has been sent out and it has been 184 acked
ACCEPTED_ON_OTHER_DEVICE, //received 'accept' from one of our own devices
ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received
ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through
ENDED, //close UI ENDED, //close UI
DECLINED_OR_BUSY, //other party declined; no retry button DECLINED_OR_BUSY, //other party declined; no retry button
CONNECTIVITY_ERROR //network error; retry button CONNECTIVITY_ERROR, //network error; retry button
APPLICATION_ERROR //something rather bad happened; libwebrtc failed or we got in IQ-error
} }

View file

@ -132,7 +132,7 @@ public class WebRTCWrapper {
); );
} }
public void initializePeerConnection(final List<PeerConnection.IceServer> iceServers) { public void initializePeerConnection(final List<PeerConnection.IceServer> iceServers) throws InitializationException {
PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory();
CameraVideoCapturer capturer = null; CameraVideoCapturer capturer = null;
@ -195,7 +195,7 @@ public class WebRTCWrapper {
final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver); final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver);
if (peerConnection == null) { if (peerConnection == null) {
throw new IllegalStateException("Unable to create PeerConnection"); throw new InitializationException("Unable to create PeerConnection");
} }
peerConnection.addStream(stream); peerConnection.addStream(stream);
peerConnection.setAudioPlayout(true); peerConnection.setAudioPlayout(true);
@ -344,6 +344,13 @@ public class WebRTCWrapper {
} }
} }
public static class InitializationException extends Exception {
private InitializationException(String message) {
super(message);
}
}
public interface EventCallback { public interface EventCallback {
void onIceCandidate(IceCandidate iceCandidate); void onIceCandidate(IceCandidate iceCandidate);

View file

@ -5,7 +5,7 @@ import android.support.annotation.NonNull;
import com.google.common.base.CaseFormat; import com.google.common.base.CaseFormat;
public enum Reason { public enum Reason {
SUCCESS, DECLINE, BUSY, CANCEL, CONNECTIVITY_ERROR, FAILED_TRANSPORT, TIMEOUT, UNKNOWN; SUCCESS, DECLINE, BUSY, CANCEL, CONNECTIVITY_ERROR, FAILED_TRANSPORT, FAILED_APPLICATION, TIMEOUT, UNKNOWN;
public static Reason of(final String value) { public static Reason of(final String value) {
try { try {

View file

@ -31,6 +31,18 @@ abstract public class AbstractAcknowledgeableStanza extends AbstractStanza {
return null; return null;
} }
public String getErrorCondition() {
Element error = findChild("error");
if (error != null) {
for(Element element : error.getChildren()) {
if (!element.getName().equals("text")) {
return element.getName();
}
}
}
return null;
}
public boolean valid() { public boolean valid() {
return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo()); return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo());
} }

View file

@ -898,6 +898,7 @@
<string name="rtp_state_ringing">Ringing</string> <string name="rtp_state_ringing">Ringing</string>
<string name="rtp_state_declined_or_busy">Busy</string> <string name="rtp_state_declined_or_busy">Busy</string>
<string name="rtp_state_connectivity_error">Unable to connect call</string> <string name="rtp_state_connectivity_error">Unable to connect call</string>
<string name="rtp_state_application_failure">Application failure</string>
<plurals name="view_users"> <plurals name="view_users">
<item quantity="one">View %1$d Participant</item> <item quantity="one">View %1$d Participant</item>
<item quantity="other">View %1$d Participants</item> <item quantity="other">View %1$d Participants</item>