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

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java                      |   5 
src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java       |   3 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java        |  19 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java            | 154 
src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java                |   4 
src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java                  |  11 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java                 |   2 
src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java |  12 
src/main/res/values/strings.xml                                                      |   1 
9 files changed, 174 insertions(+), 37 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java 🔗

@@ -169,6 +169,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
             case CONNECTIVITY_ERROR:
                 binding.status.setText(R.string.rtp_state_connectivity_error);
                 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.show();
             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.setImageResource(R.drawable.ic_clear_white_48dp);
             this.binding.rejectCall.show();

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

@@ -95,6 +95,7 @@ public abstract class AbstractJingleConnection {
         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_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
     }
 }

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

@@ -55,22 +55,27 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)) {
                 connection = new JingleRtpConnection(this, id, from);
             } else {
-                //TODO return feature-not-implemented
+                respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel");
                 return;
             }
             connections.put(id, connection);
             connection.deliverPacket(packet);
         } else {
             Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet);
-            final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
-            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);
+            respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
+
         }
     }
 
+    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) {
         Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace()));
         final String sessionId = message.getAttribute("id");

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

@@ -34,12 +34,40 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
 
     static {
         final ImmutableMap.Builder<State, Collection<State>> transitionBuilder = new ImmutableMap.Builder<>();
-        transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED));
-        transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED, State.RETRACTED));
-        transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED_PRE_APPROVED, State.TERMINATED_SUCCESS));
-        transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY));
-        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.NULL, ImmutableList.of(
+                State.PROPOSED,
+                State.SESSION_INITIALIZED,
+                State.TERMINATED_APPLICATION_FAILURE
+        ));
+        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();
     }
 
@@ -64,6 +92,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
             case CANCEL:
             case TIMEOUT:
                 return State.TERMINATED_CANCEL_OR_TIMEOUT;
+            case FAILED_APPLICATION:
+                return State.TERMINATED_APPLICATION_FAILURE;
             default:
                 return State.TERMINATED_CONNECTIVITY_ERROR;
         }
@@ -86,12 +116,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
                 receiveSessionTerminate(jinglePacket);
                 break;
             default:
+                respondOk(jinglePacket);
                 Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction()));
                 break;
         }
     }
 
     private void receiveSessionTerminate(final JinglePacket jinglePacket) {
+        respondOk(jinglePacket);
         final Reason reason = jinglePacket.getReason();
         final State previous = this.state;
         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) {
         if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
+            respondOk(jinglePacket);
             final RtpContentMap contentMap;
             try {
                 contentMap = RtpContentMap.of(jinglePacket);
             } 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;
             }
             final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap;
@@ -136,13 +169,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
             }
         } else {
             Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state);
+            respondWithOutOfOrder(jinglePacket);
         }
     }
 
     private void receiveSessionInitiate(final JinglePacket jinglePacket) {
         if (isInitiator()) {
             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;
         }
         final RtpContentMap contentMap;
@@ -150,6 +184,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
             contentMap = RtpContentMap.of(jinglePacket);
             contentMap.requireContentDescriptions();
         } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) {
+            respondOk(jinglePacket);
+            sendSessionTerminate(Reason.FAILED_APPLICATION);
             Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e);
             return;
         }
@@ -161,6 +197,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
             target = State.SESSION_INITIALIZED;
         }
         if (transition(target)) {
+            respondOk(jinglePacket);
             this.initiatorRtpContentMap = contentMap;
             if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
                 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
@@ -171,13 +208,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
             }
         } else {
             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) {
         if (!isInitiator()) {
             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;
         }
         final RtpContentMap contentMap;
@@ -185,28 +223,43 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
             contentMap = RtpContentMap.of(jinglePacket);
             contentMap.requireContentDescriptions();
         } 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;
         }
         Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents");
         if (transition(State.SESSION_ACCEPTED)) {
+            respondOk(jinglePacket);
             receiveSessionAccept(contentMap);
         } else {
             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) {
         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.Type.ANSWER,
-                SessionDescription.of(contentMap).toString()
+                sessionDescription.toString()
         );
         try {
             this.webRTCWrapper.setRemoteDescription(answer).get();
         } 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 {
             offer = SessionDescription.of(rtpContentMap);
         } catch (final IllegalArgumentException e) {
-            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to process offer", e);
-            //TODO terminate session with application error
+            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-initiate to SDP", e);
+            webRTCWrapper.close();
+            sendSessionTerminate(Reason.FAILED_APPLICATION);
+            ;
             return;
         }
         sendSessionAccept(offer);
@@ -228,7 +283,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
 
     private void sendSessionAccept(SessionDescription offer) {
         discoverIceServers(iceServers -> {
-            setupWebRTC(iceServers);
+            try {
+                setupWebRTC(iceServers);
+            } catch (WebRTCWrapper.InitializationException e) {
+                sendSessionTerminate(Reason.FAILED_APPLICATION);
+                return;
+            }
             final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(
                     org.webrtc.SessionDescription.Type.OFFER,
                     offer.toString()
@@ -351,7 +411,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
                 this.jingleConnectionManager.finishConnection(this);
             }
         } 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));
         }
     }
@@ -374,7 +433,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
     private void sendSessionInitiate(final State targetState) {
         Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
         discoverIceServers(iceServers -> {
-            setupWebRTC(iceServers);
+            try {
+                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 {
                 org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get();
                 final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
@@ -382,8 +447,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
                 final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
                 sendSessionInitiate(rtpContentMap, targetState);
                 this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
-            } catch (Exception e) {
-                Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e);
+            } catch (final Exception 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) {
         jinglePacket.setTo(id.with);
-        //TODO track errors
-        xmppConnectionService.sendIqPacket(id.account, jinglePacket, null);
+        xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> {
+            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() {
@@ -474,6 +580,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
                 return RtpEndUserState.ENDED;
             case TERMINATED_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));
     }
@@ -526,7 +634,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         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.initializePeerConnection(iceServers);
     }

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

@@ -6,10 +6,10 @@ public enum RtpEndUserState {
     CONNECTED, //session-accepted and webrtc peer connection is connected
     FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
     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
     ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through
     ENDED, //close UI
     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
 }

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

@@ -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();
 
         CameraVideoCapturer capturer = null;
@@ -195,7 +195,7 @@ public class WebRTCWrapper {
 
         final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver);
         if (peerConnection == null) {
-            throw new IllegalStateException("Unable to create PeerConnection");
+            throw new InitializationException("Unable to create PeerConnection");
         }
         peerConnection.addStream(stream);
         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 {
         void onIceCandidate(IceCandidate iceCandidate);
 

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

@@ -5,7 +5,7 @@ import android.support.annotation.NonNull;
 import com.google.common.base.CaseFormat;
 
 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) {
 		try {

src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java 🔗

@@ -31,6 +31,18 @@ abstract public class AbstractAcknowledgeableStanza extends AbstractStanza {
 		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() {
 		return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo());
 	}

src/main/res/values/strings.xml 🔗

@@ -898,6 +898,7 @@
     <string name="rtp_state_ringing">Ringing</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_application_failure">Application failure</string>
     <plurals name="view_users">
         <item quantity="one">View %1$d Participant</item>
         <item quantity="other">View %1$d Participants</item>