make retract jingle messages work

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java          |   9 
src/main/java/eu/siacs/conversations/services/NotificationService.java        |   2 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java      |   6 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java             |   6 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java               | 107 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java |  70 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java     |  25 
src/main/res/values/strings.xml                                               |   2 
8 files changed, 187 insertions(+), 40 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -244,4 +244,13 @@ public class MessageGenerator extends AbstractGenerator {
 		packet.addChild("request", "urn:xmpp:receipts");
 		return packet;
 	}
+
+	public MessagePacket sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) {
+		final MessagePacket packet = new MessagePacket();
+		packet.setTo(proposal.with);
+		final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
+		propose.setAttribute("id", proposal.sessionId);
+		propose.addChild("description", Namespace.JINGLE_APPS_RTP);
+		return packet;
+	}
 }

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

@@ -351,7 +351,7 @@ public class NotificationService {
         builder.addAction(new NotificationCompat.Action.Builder(
                 R.drawable.ic_call_white_24dp,
                 mXmppConnectionService.getString(R.string.answer_call),
-                createPendingRtpSession(id, RtpSessionActivity.ACTION_ACCEPT, 103))
+                createPendingRtpSession(id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103))
                 .build());
         final Notification notification = builder.build();
         notification.flags = notification.flags | Notification.FLAG_INSISTENT;

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

@@ -3977,9 +3977,9 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void notifyJingleRtpConnectionUpdate(final Account account, final Jid with, final RtpEndUserState state) {
+    public void notifyJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
         for(OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) {
-            listener.onJingleRtpConnectionUpdate(account, with, state);
+            listener.onJingleRtpConnectionUpdate(account, with, sessionId, state);
         }
     }
 
@@ -4661,7 +4661,7 @@ public class XmppConnectionService extends Service {
     }
 
     public interface OnJingleRtpConnectionUpdate {
-        void onJingleRtpConnectionUpdate(final Account account, final Jid with, final RtpEndUserState state);
+        void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state);
     }
 
     public interface OnAccountUpdate {

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

@@ -1243,7 +1243,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 
     private void triggerRtpSession() {
         final Contact contact = conversation.getContact();
-        activity.xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(conversation.getAccount(), contact);
+        final Intent intent = new Intent(activity, RtpSessionActivity.class);
+        intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
+        intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, contact.getAccount().getJid().toEscapedString());
+        intent.putExtra(RtpSessionActivity.EXTRA_WITH, contact.getJid().asBareJid().toEscapedString());
+        startActivity(intent);
     }
 
     private void handleAttachmentSelection(MenuItem item) {

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

@@ -8,6 +8,7 @@ import android.view.View;
 import android.view.WindowManager;
 
 import java.lang.ref.WeakReference;
+import java.util.Arrays;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -20,12 +21,16 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 import rocks.xmpp.addr.Jid;
 
+import static java.util.Arrays.asList;
+
 public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate {
 
     public static final String EXTRA_WITH = "with";
     public static final String EXTRA_SESSION_ID = "session_id";
 
-    public static final String ACTION_ACCEPT = "accept";
+    public static final String ACTION_ACCEPT_CALL = "action_accept_call";
+    public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call";
+    public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call";
 
     private WeakReference<JingleRtpConnection> rtpConnectionReference;
 
@@ -53,7 +58,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
     }
 
     private void endCall(View view) {
-        requireRtpConnection().endCall();
+        if (this.rtpConnectionReference == null) {
+            final Intent intent = getIntent();
+            final Account account = extractAccount(intent);
+            final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
+            xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
+            finish();
+        } else {
+            requireRtpConnection().endCall();
+        }
     }
 
     private void rejectCall(View view) {
@@ -73,8 +86,8 @@ 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()");
+        if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
+            Log.d(Config.LOGTAG, "accepting through onNewIntent()");
             requireRtpConnection().acceptCall();
         }
     }
@@ -83,28 +96,50 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
     void onBackendConnected() {
         final Intent intent = getIntent();
         final Account account = extractAccount(intent);
-        final String with = intent.getStringExtra(EXTRA_WITH);
+        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
         final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
-        if (with != null && sessionId != null) {
-            final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
-                    .findJingleRtpConnection(account, Jid.ofEscaped(with), sessionId);
-            if (reference == null || reference.get() == null) {
-                finish();
-                return;
-            }
-            this.rtpConnectionReference = reference;
-            binding.with.setText(getWith().getDisplayName());
-            final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
-            final String action = intent.getAction();
-            updateStateDisplay(currentState);
-            updateButtonConfiguration(currentState);
-            if (ACTION_ACCEPT.equals(action)) {
-                Log.d(Config.LOGTAG,"intent action was accept");
+        if (sessionId != null) {
+            initializeActivityWithRunningRapSession(account, with, sessionId);
+            if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
+                Log.d(Config.LOGTAG, "intent action was accept");
                 requireRtpConnection().acceptCall();
             }
+        } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) {
+            xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with);
+            binding.with.setText(account.getRoster().getContact(with).getDisplayName());
         }
     }
 
+
+    private void initializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
+        final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
+                .findJingleRtpConnection(account, with, sessionId);
+        if (reference == null || reference.get() == null) {
+            finish();
+            return;
+        }
+        this.rtpConnectionReference = reference;
+        final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
+        if (currentState == RtpEndUserState.ENDED) {
+            finish();
+            return;
+        }
+        binding.with.setText(getWith().getDisplayName());
+        updateStateDisplay(currentState);
+        updateButtonConfiguration(currentState);
+    }
+
+    private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
+        runOnUiThread(() -> {
+            initializeActivityWithRunningRapSession(account, with, sessionId);
+        });
+        final Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
+        intent.putExtra(EXTRA_WITH, with.toEscapedString());
+        intent.putExtra(EXTRA_SESSION_ID, sessionId);
+        setIntent(intent);
+    }
+
     private void updateStateDisplay(final RtpEndUserState state) {
         switch (state) {
             case INCOMING_CALL:
@@ -122,6 +157,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
             case ENDING_CALL:
                 binding.status.setText(R.string.rtp_state_ending_call);
                 break;
+            case FINDING_DEVICE:
+                binding.status.setText(R.string.rtp_state_finding_device);
+                break;
+            case RINGING:
+                binding.status.setText(R.string.rtp_state_ringing);
         }
     }
 
@@ -156,9 +196,19 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
     }
 
     @Override
-    public void onJingleRtpConnectionUpdate(Account account, Jid with, RtpEndUserState state) {
+    public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
+        Log.d(Config.LOGTAG,"onJingleRtpConnectionUpdate("+state+")");
+        if (with.isBareJid()) {
+            updateRtpSessionProposalState(with, state);
+            return;
+        }
+        if (this.rtpConnectionReference == null) {
+            //this happens when going from proposed session to actual session
+            reInitializeActivityWithRunningRapSession(account, with, sessionId);
+            return;
+        }
         final AbstractJingleConnection.Id id = requireRtpConnection().getId();
-        if (account == id.account && id.with.equals(with)) {
+        if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
             if (state == RtpEndUserState.ENDED) {
                 finish();
                 return;
@@ -170,6 +220,19 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
         } else {
             Log.d(Config.LOGTAG, "received update for other rtp session");
         }
+    }
 
+    private void updateRtpSessionProposalState(Jid with, RtpEndUserState state) {
+        final Intent intent = getIntent();
+        final String intentExtraWith = intent == null ? null : intent.getStringExtra(EXTRA_WITH);
+        if (intentExtraWith == null) {
+            return;
+        }
+        if (Jid.ofEscaped(intentExtraWith).asBareJid().equals(with)) {
+            runOnUiThread(() -> {
+                updateStateDisplay(state);
+                updateButtonConfiguration(state);
+            });
+        }
     }
 }

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

@@ -188,12 +188,52 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         }
     }
 
-    public void proposeJingleRtpSession(final Account account, final Contact contact) {
-        final RtpSessionProposal proposal = RtpSessionProposal.of(account, contact.getJid().asBareJid());
+    public void retractSessionProposal(final Account account, final Jid with) {
         synchronized (this.rtpSessionProposals) {
+            RtpSessionProposal matchingProposal = null;
+            for (RtpSessionProposal proposal : this.rtpSessionProposals.keySet()) {
+                if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
+                    matchingProposal = proposal;
+                    break;
+                }
+            }
+            if (matchingProposal != null) {
+                this.rtpSessionProposals.remove(matchingProposal);
+                final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(matchingProposal);
+                Log.d(Config.LOGTAG, messagePacket.toString());
+                mXmppConnectionService.sendMessagePacket(account, messagePacket);
+
+            }
+        }
+    }
+
+    public void proposeJingleRtpSession(final Account account, final Jid with) {
+        synchronized (this.rtpSessionProposals) {
+            for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
+                RtpSessionProposal proposal = entry.getKey();
+                if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
+                    final DeviceDiscoveryState preexistingState = entry.getValue();
+                    if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) {
+                        mXmppConnectionService.notifyJingleRtpConnectionUpdate(
+                                account,
+                                with,
+                                proposal.sessionId,
+                                preexistingState.toEndUserState()
+                        );
+                        return;
+                    }
+                }
+            }
+            final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid());
             this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
+            mXmppConnectionService.notifyJingleRtpConnectionUpdate(
+                    account,
+                    proposal.with,
+                    proposal.sessionId,
+                    RtpEndUserState.FINDING_DEVICE
+            );
             final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
-            Log.d(Config.LOGTAG,messagePacket.toString());
+            Log.d(Config.LOGTAG, messagePacket.toString());
             mXmppConnectionService.sendMessagePacket(account, messagePacket);
         }
     }
@@ -255,24 +295,25 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     }
 
     public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) {
-        final RtpSessionProposal sessionProposal = new RtpSessionProposal(account,from.asBareJid(),sessionId);
+        final RtpSessionProposal sessionProposal = new RtpSessionProposal(account, from.asBareJid(), sessionId);
         synchronized (this.rtpSessionProposals) {
             final DeviceDiscoveryState currentState = rtpSessionProposals.get(sessionProposal);
             if (currentState == null) {
-                Log.d(Config.LOGTAG,"unable to find session proposal for session id "+sessionId);
+                Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId);
                 return;
             }
             if (currentState == DeviceDiscoveryState.DISCOVERED) {
-                Log.d(Config.LOGTAG,"session proposal already at discovered. not going to fall back");
+                Log.d(Config.LOGTAG, "session proposal already at discovered. not going to fall back");
                 return;
             }
             this.rtpSessionProposals.put(sessionProposal, target);
-            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": flagging session "+sessionId+" as "+target);
+            mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, target.toEndUserState());
+            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target);
         }
     }
 
     public void rejectRtpSession(final String sessionId) {
-        for(final AbstractJingleConnection connection : this.connections.values()) {
+        for (final AbstractJingleConnection connection : this.connections.values()) {
             if (connection.getId().sessionId.equals(sessionId)) {
                 if (connection instanceof JingleRtpConnection) {
                     ((JingleRtpConnection) connection).rejectCall();
@@ -313,6 +354,17 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     }
 
     public enum DeviceDiscoveryState {
-        SEARCHING, DISCOVERED, FAILED
+        SEARCHING, DISCOVERED, FAILED;
+
+        public RtpEndUserState toEndUserState() {
+            switch (this) {
+                case SEARCHING:
+                    return RtpEndUserState.FINDING_DEVICE;
+                case DISCOVERED:
+                    return RtpEndUserState.RINGING;
+                default:
+                    return RtpEndUserState.CONNECTIVITY_ERROR;
+            }
+        }
     }
 }

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

@@ -1,6 +1,5 @@
 package eu.siacs.conversations.xmpp.jingle;
 
-import android.content.Intent;
 import android.util.Log;
 
 import com.google.common.collect.ImmutableList;
@@ -17,7 +16,6 @@ import java.util.List;
 import java.util.Map;
 
 import eu.siacs.conversations.Config;
-import eu.siacs.conversations.ui.RtpSessionActivity;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
@@ -34,7 +32,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, State.REJECTED));
+        transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED, State.RETRACTED));
         transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED));
         transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED));
         VALID_TRANSITIONS = transitionBuilder.build();
@@ -234,6 +232,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
                 break;
             case "proceed":
                 receiveProceed(from, message);
+                break;
+            case "retract":
+                receiveRetract(from, message);
+                break;
             default:
                 break;
         }
@@ -272,6 +274,21 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
     }
 
+    private void receiveRetract(final Jid from, final Element retract) {
+        if (from.equals(id.with)) {
+            if (transition(State.RETRACTED)) {
+                xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
+                Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted");
+                //TODO create missed call notification/message
+                jingleConnectionManager.finishConnection(this);
+            } else {
+                Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state);
+            }
+        } else {
+            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received retract from " + from + ". expected retract from" + id.with + ". ignoring");
+        }
+    }
+
     private void sendSessionInitiate() {
         Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
         setupWebRTC();
@@ -472,6 +489,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
     }
 
     private void updateEndUserState() {
-        xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, getEndUserState());
+        xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, getEndUserState());
     }
 }

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

@@ -894,6 +894,8 @@
     <string name="rtp_state_ending_call">Ending call</string>
     <string name="answer_call">Answer</string>
     <string name="dismiss_call">Dismiss</string>
+    <string name="rtp_state_finding_device">Locating devices</string>
+    <string name="rtp_state_ringing">Ringing</string>
     <plurals name="view_users">
         <item quantity="one">View %1$d Participant</item>
         <item quantity="other">View %1$d Participants</item>