prepare more state transitions

Daniel Gultsch created

Change summary

src/main/AndroidManifest.xml                                                       |  3 
src/main/java/eu/siacs/conversations/Config.java                                   |  2 
src/main/java/eu/siacs/conversations/services/NotificationService.java             |  4 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java           |  1 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java                    | 13 
src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java     |  7 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java      | 10 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java | 41 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java          | 85 
src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java              |  4 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java         | 12 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java               | 27 
12 files changed, 157 insertions(+), 52 deletions(-)

Detailed changes

src/main/AndroidManifest.xml 🔗

@@ -289,7 +289,8 @@
         <activity
             android:name=".ui.ChannelDiscoveryActivity"
             android:label="@string/discover_channels" />
-        <activity android:name=".ui.RtpSessionActivity" />
+        <activity android:name=".ui.RtpSessionActivity"
+            android:launchMode="singleTop"/>
     </application>
 
 </manifest>

src/main/java/eu/siacs/conversations/Config.java 🔗

@@ -105,7 +105,7 @@ public final class Config {
 
     public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
     public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
-    public static final boolean DISABLE_HTTP_UPLOAD = true;
+    public static final boolean DISABLE_HTTP_UPLOAD = false;
     public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
     public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background
     public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = true; //setting to true might increase power consumption

src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -364,8 +364,8 @@ public class NotificationService {
         fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString());
         fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
         fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
-        fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        //fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        //fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
         return PendingIntent.getActivity(mXmppConnectionService, requestCode, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
     }
 

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -643,6 +643,7 @@ public class XmppConnectionService extends Service {
                 case ACTION_DISMISS_CALL:
                     final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
                     Log.d(Config.LOGTAG,"received intent to dismiss call with session id "+sessionId);
+                    mJingleConnectionManager.rejectRtpSession(sessionId);
                     break;
                 case ACTION_DISMISS_ERROR_NOTIFICATIONS:
                     dismissErrorNotifications();

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

@@ -70,6 +70,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
 
     }
 
+    @Override
+    public void onNewIntent(final Intent intent) {
+        super.onNewIntent(intent);
+        if (ACTION_ACCEPT.equals(intent.getAction())) {
+            Log.d(Config.LOGTAG,"accepting through onNewIntent()");
+            requireRtpConnection().acceptCall();
+        }
+    }
+
     @Override
     void onBackendConnected() {
         final Intent intent = getIntent();
@@ -150,6 +159,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
     public void onJingleRtpConnectionUpdate(Account account, Jid with, RtpEndUserState state) {
         final AbstractJingleConnection.Id id = requireRtpConnection().getId();
         if (account == id.account && id.with.equals(with)) {
+            if (state == RtpEndUserState.ENDED) {
+                finish();
+                return;
+            }
             runOnUiThread(() -> {
                 updateStateDisplay(state);
                 updateButtonConfiguration(state);

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

@@ -87,8 +87,13 @@ public abstract class AbstractJingleConnection {
         PROPOSED,
         ACCEPTED,
         PROCEED,
+        REJECTED,
+        RETRACTED,
         SESSION_INITIALIZED, //equal to 'PENDING'
         SESSION_ACCEPTED, //equal to 'ACTIVE'
-        TERMINATED //equal to 'ENDED'
+        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
     }
 }

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

@@ -271,6 +271,16 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         }
     }
 
+    public void rejectRtpSession(final String sessionId) {
+        for(final AbstractJingleConnection connection : this.connections.values()) {
+            if (connection.getId().sessionId.equals(sessionId)) {
+                if (connection instanceof JingleRtpConnection) {
+                    ((JingleRtpConnection) connection).rejectCall();
+                }
+            }
+        }
+    }
+
     public static class RtpSessionProposal {
         private final Account account;
         public final Jid with;

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

@@ -156,7 +156,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
 
         @Override
         public void onFileTransferAborted() {
-            JingleFileTransferConnection.this.sendSessionTerminate("connectivity-error");
+            JingleFileTransferConnection.this.sendSessionTerminate(Reason.CONNECTIVITY_ERROR);
             JingleFileTransferConnection.this.fail();
         }
     };
@@ -245,23 +245,20 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         if (action == JinglePacket.Action.SESSION_INITIATE) {
             init(packet);
         } else if (action == JinglePacket.Action.SESSION_TERMINATE) {
-            Reason reason = packet.getReason();
-            if (reason != null) {
-                if (reason.hasChild("cancel")) {
+            final Reason reason = packet.getReason();
+            switch (reason) {
+                case CANCEL:
                     this.cancelled = true;
                     this.fail();
-                } else if (reason.hasChild("success")) {
+                    break;
+                case SUCCESS:
                     this.receiveSuccess();
-                } else {
-                    final List<Element> children = reason.getChildren();
-                    if (children.size() == 1) {
-                        this.fail(children.get(0).getName());
-                    } else {
-                        this.fail();
-                    }
-                }
-            } else {
-                this.fail();
+                    break;
+                default:
+                    Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate with reason " + reason);
+                    this.fail();
+                    break;
+
             }
         } else if (action == JinglePacket.Action.SESSION_ACCEPT) {
             receiveAccept(packet);
@@ -756,7 +753,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
                         connection.setActivated(true);
                     } else {
                         Log.d(Config.LOGTAG, "activated connection not found");
-                        sendSessionTerminate("failed-transport");
+                        sendSessionTerminate(Reason.FAILED_TRANSPORT);
                         this.fail();
                     }
                 }
@@ -894,7 +891,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
     }
 
     private void sendSuccess() {
-        sendSessionTerminate("success");
+        sendSessionTerminate(Reason.SUCCESS);
         this.disconnectSocks5Connections();
         this.mJingleStatus = JINGLE_STATUS_FINISHED;
         this.message.setStatus(Message.STATUS_RECEIVED);
@@ -1014,10 +1011,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
     @Override
     public void cancel() {
         this.cancelled = true;
-        abort("cancel");
+        abort(Reason.CANCEL);
     }
 
-    void abort(final String reason) {
+    void abort(final Reason reason) {
         this.disconnectSocks5Connections();
         if (this.transport instanceof JingleInBandTransport) {
             this.transport.disconnect();
@@ -1065,11 +1062,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         this.jingleConnectionManager.finishConnection(this);
     }
 
-    private void sendSessionTerminate(String reason) {
+    private void sendSessionTerminate(Reason reason) {
         final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_TERMINATE);
-        final Reason r = new Reason();
-        r.addChild(reason);
-        packet.setReason(r);
+        packet.setReason(reason);
         this.sendJinglePacket(packet);
     }
 

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

@@ -23,6 +23,7 @@ import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 import rocks.xmpp.addr.Jid;
 
@@ -33,7 +34,7 @@ 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));
+        transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED));
         transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED));
         transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED));
         VALID_TRANSITIONS = transitionBuilder.build();
@@ -63,12 +64,36 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
             case SESSION_ACCEPT:
                 receiveSessionAccept(jinglePacket);
                 break;
+            case SESSION_TERMINATE:
+                receiveSessionTerminate(jinglePacket);
+                break;
             default:
                 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) {
+        final Reason reason = jinglePacket.getReason();
+        switch (reason) {
+            case SUCCESS:
+                transitionOrThrow(State.TERMINATED_SUCCESS);
+                break;
+            case DECLINE:
+            case BUSY:
+                transitionOrThrow(State.TERMINATED_DECLINED_OR_BUSY);
+                break;
+            case CANCEL:
+            case TIMEOUT:
+                transitionOrThrow(State.TERMINATED_CANCEL_OR_TIMEOUT);
+                break;
+            default:
+                transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
+                break;
+        }
+        jingleConnectionManager.finishConnection(this);
+    }
+
     private void receiveTransportInfo(final JinglePacket jinglePacket) {
         if (isInState(State.SESSION_INITIALIZED, State.SESSION_ACCEPTED)) {
             final RtpContentMap contentMap;
@@ -315,10 +340,24 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
                 } else if (state == PeerConnection.PeerConnectionState.CLOSED) {
                     return RtpEndUserState.ENDING_CALL;
                 } else {
-                    return RtpEndUserState.FAILED;
+                    return RtpEndUserState.ENDING_CALL;
+                }
+            case REJECTED:
+            case TERMINATED_DECLINED_OR_BUSY:
+                if (isInitiator()) {
+                    return RtpEndUserState.DECLINED_OR_BUSY;
+                } else {
+                    return RtpEndUserState.ENDED;
                 }
+            case TERMINATED_SUCCESS:
+            case ACCEPTED:
+            case RETRACTED:
+            case TERMINATED_CANCEL_OR_TIMEOUT:
+                return RtpEndUserState.ENDED;
+            case TERMINATED_CONNECTIVITY_ERROR:
+                return RtpEndUserState.CONNECTIVITY_ERROR;
         }
-        return RtpEndUserState.FAILED;
+        throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state));
     }
 
 
@@ -331,19 +370,34 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
                 acceptCallFromSessionInitialized();
                 break;
             default:
-                throw new IllegalStateException("Can not pick up call from " + this.state);
+                throw new IllegalStateException("Can not accept call from " + this.state);
         }
     }
 
     public void rejectCall() {
-        Log.d(Config.LOGTAG, "todo rejecting call");
-        xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
+        switch (this.state) {
+            case PROPOSED:
+                rejectCallFromProposed();
+                break;
+            default:
+                throw new IllegalStateException("Can not reject call from " + this.state);
+        }
     }
 
     public void endCall() {
+
+        //TODO from `propose` we call `retract`
+
         if (isInState(State.SESSION_INITIALIZED, State.SESSION_ACCEPTED)) {
+            //TODO during session_initialized we might not have a peer connection yet (if the session was initialized directly)
+
+            //TODO from session_initialized we call `cancel`
+
+            //TODO from session_accepted we call `success`
+
             webRTCWrapper.close();
         } else {
+            //TODO during earlier stages we want to retract the proposal etc
             Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": called 'endCall' while in state " + this.state);
         }
     }
@@ -356,10 +410,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
     private void acceptCallFromProposed() {
         transitionOrThrow(State.PROCEED);
         xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
+        //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916
+        this.sendJingleMessage("proceed");
+
+        //TODO send `accept` to self
+    }
+
+    private void rejectCallFromProposed() {
+        transitionOrThrow(State.REJECTED);
+        xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
+        this.sendJingleMessage("reject");
+        jingleConnectionManager.finishConnection(this);
+    }
+
+    private void sendJingleMessage(final String action) {
         final MessagePacket messagePacket = new MessagePacket();
         messagePacket.setTo(id.with);
-        //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916
-        messagePacket.addChild("proceed", Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId);
+        messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId);
         Log.d(Config.LOGTAG, messagePacket.toString());
         xmppConnectionService.sendMessagePacket(id.account, messagePacket);
     }
@@ -400,7 +467,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
 
     @Override
     public void onConnectionChange(PeerConnection.PeerConnectionState newState) {
-        Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": PeerConnectionState changed to "+newState);
+        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState);
         updateEndUserState();
     }
 

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

@@ -9,5 +9,7 @@ public enum RtpEndUserState {
     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
-    FAILED //something went wrong. TODO needs more concrete error states
+    ENDED, //close UI
+    DECLINED_OR_BUSY, //other party declined; no retry button
+    CONNECTIVITY_ERROR //network error; retry button
 }

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

@@ -70,12 +70,20 @@ public class JinglePacket extends IqPacket {
 
     public Reason getReason() {
         final Element reason = getJingleChild("reason");
-        return reason == null ? null : Reason.upgrade(reason);
+        if (reason == null) {
+            return Reason.UNKNOWN;
+        }
+        for(Element child : reason.getChildren()) {
+            if (!"text".equals(child.getName())) {
+                return Reason.of(child.getName());
+            }
+        }
+        return Reason.UNKNOWN;
     }
 
     public void setReason(final Reason reason) {
         final Element jingle = findChild("jingle", Namespace.JINGLE);
-        jingle.addChild(reason);
+        jingle.addChild(new Element("reason").addChild(reason.toString()));
     }
 
     //RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise

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

@@ -1,20 +1,23 @@
 package eu.siacs.conversations.xmpp.jingle.stanzas;
 
-import com.google.common.base.Preconditions;
+import android.support.annotation.NonNull;
 
-import eu.siacs.conversations.xml.Element;
+import com.google.common.base.CaseFormat;
 
-public class Reason extends Element {
+public enum Reason {
+	SUCCESS, DECLINE, BUSY, CANCEL, CONNECTIVITY_ERROR, FAILED_TRANSPORT, TIMEOUT, UNKNOWN;
 
-	public Reason() {
-		super("reason");
+	public static Reason of(final String value) {
+		try {
+			return Reason.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value));
+		} catch (Exception e) {
+			return UNKNOWN;
+		}
 	}
 
-	public static Reason upgrade(final Element element) {
-		Preconditions.checkArgument("reason".equals(element.getName()));
-		final Reason reason = new Reason();
-		reason.setAttributes(element.getAttributes());
-		reason.setChildren(element.getChildren());
-		return reason;
+	@Override
+	@NonNull
+	public String toString() {
+		return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString());
 	}
-}
+}