From 268eedad8902dcbdab391395fe8d8d2d445a4ea9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 9 Apr 2020 15:22:03 +0200 Subject: [PATCH] proper iq tracing (handling of errors); responding to all iqs --- .../conversations/ui/RtpSessionActivity.java | 5 +- .../xmpp/jingle/AbstractJingleConnection.java | 3 +- .../xmpp/jingle/JingleConnectionManager.java | 19 ++- .../xmpp/jingle/JingleRtpConnection.java | 154 +++++++++++++++--- .../xmpp/jingle/RtpEndUserState.java | 4 +- .../xmpp/jingle/WebRTCWrapper.java | 11 +- .../xmpp/jingle/stanzas/Reason.java | 2 +- .../AbstractAcknowledgeableStanza.java | 12 ++ src/main/res/values/strings.xml | 1 + 9 files changed, 174 insertions(+), 37 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 7eb4908fb..232ceb8b2 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/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(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 32a52fe79..6cf7d2a91 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/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 } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 5e2e59b29..88ce13d6e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/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"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index bc32c3838..153b0172f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/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> 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 iceServers) { + private void setupWebRTC(final List iceServers) throws WebRTCWrapper.InitializationException { this.webRTCWrapper.setup(this.xmppConnectionService); this.webRTCWrapper.initializePeerConnection(iceServers); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index e8d8bd5d2..4baa0019d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/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 } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 1688a8ecb..8eb46fe1e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -132,7 +132,7 @@ public class WebRTCWrapper { ); } - public void initializePeerConnection(final List iceServers) { + public void initializePeerConnection(final List 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); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java index 070f77226..635f26d54 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java +++ b/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 { diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java index 095075616..552f40598 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java +++ b/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()); } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 487ee434b..39b9a3337 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -898,6 +898,7 @@ Ringing Busy Unable to connect call + Application failure View %1$d Participant View %1$d Participants