JingleRtpConnection.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import android.util.Log;
  4
  5import com.google.common.base.Function;
  6import com.google.common.collect.ImmutableList;
  7import com.google.common.collect.ImmutableMap;
  8import com.google.common.collect.Maps;
  9
 10import org.checkerframework.checker.nullness.compatqual.NullableDecl;
 11
 12import java.util.Collection;
 13import java.util.Map;
 14
 15import eu.siacs.conversations.Config;
 16import eu.siacs.conversations.xml.Element;
 17import eu.siacs.conversations.xml.Namespace;
 18import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 19import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
 20import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
 21import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 22import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 23import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 24import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 25import rocks.xmpp.addr.Jid;
 26
 27public class JingleRtpConnection extends AbstractJingleConnection {
 28
 29    private static final Map<State, Collection<State>> VALID_TRANSITIONS;
 30
 31    static {
 32        final ImmutableMap.Builder<State, Collection<State>> transitionBuilder = new ImmutableMap.Builder<>();
 33        transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED));
 34        transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED));
 35        transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED));
 36        VALID_TRANSITIONS = transitionBuilder.build();
 37    }
 38
 39    private State state = State.NULL;
 40
 41
 42    public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
 43        super(jingleConnectionManager, id, initiator);
 44    }
 45
 46    @Override
 47    void deliverPacket(final JinglePacket jinglePacket) {
 48        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection");
 49        switch (jinglePacket.getAction()) {
 50            case SESSION_INITIATE:
 51                receiveSessionInitiate(jinglePacket);
 52                break;
 53            default:
 54                Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction()));
 55                break;
 56        }
 57    }
 58
 59    private void receiveSessionInitiate(final JinglePacket jinglePacket) {
 60        if (isInitiator()) {
 61            Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid()));
 62            //TODO respond with out-of-order
 63            return;
 64        }
 65        final Map<String, DescriptionTransport> contents;
 66        try {
 67            contents = DescriptionTransport.of(jinglePacket.getJingleContents());
 68        } catch (IllegalArgumentException | NullPointerException e) {
 69            Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": improperly formatted contents",e);
 70            return;
 71        }
 72        Log.d(Config.LOGTAG,"processing session-init with "+contents.size()+" contents");
 73        final State oldState = this.state;
 74        if (transition(State.SESSION_INITIALIZED)) {
 75            if (oldState == State.PROCEED) {
 76                sendSessionAccept();
 77            } else {
 78                //TODO start ringing
 79            }
 80        } else {
 81            Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state));
 82        }
 83    }
 84
 85    void deliveryMessage(final Jid from, final Element message) {
 86        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message);
 87        switch (message.getName()) {
 88            case "propose":
 89                receivePropose(from, message);
 90                break;
 91            case "proceed":
 92                receiveProceed(from, message);
 93            default:
 94                break;
 95        }
 96    }
 97
 98    private void receivePropose(final Jid from, final Element propose) {
 99        final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid());
100        if (originatedFromMyself) {
101            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring");
102        } else if (transition(State.PROPOSED)) {
103            //TODO start ringing or something
104            pickUpCall();
105        } else {
106            Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state);
107        }
108    }
109
110    private void receiveProceed(final Jid from, final Element proceed) {
111        if (from.equals(id.with)) {
112            if (isInitiator()) {
113                if (transition(State.SESSION_INITIALIZED)) {
114                    this.sendSessionInitiate();
115                } else {
116                    Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state));
117                }
118            } else {
119                Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because we were not initializing", id.account.getJid().asBareJid()));
120            }
121        } else {
122            Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with));
123        }
124    }
125
126    private void sendSessionInitiate() {
127
128    }
129
130    private void sendSessionAccept() {
131        Log.d(Config.LOGTAG,"sending session-accept");
132    }
133
134    public void pickUpCall() {
135        switch (this.state) {
136            case PROPOSED:
137                pickupCallFromProposed();
138                break;
139            case SESSION_INITIALIZED:
140                pickupCallFromSessionInitialized();
141                break;
142            default:
143                throw new IllegalStateException("Can not pick up call from " + this.state);
144        }
145    }
146
147    private void pickupCallFromProposed() {
148        transitionOrThrow(State.PROCEED);
149        final MessagePacket messagePacket = new MessagePacket();
150        messagePacket.setTo(id.with);
151        //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916
152        messagePacket.addChild("accept", Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId);
153        Log.d(Config.LOGTAG, messagePacket.toString());
154        xmppConnectionService.sendMessagePacket(id.account, messagePacket);
155    }
156
157    private void pickupCallFromSessionInitialized() {
158
159    }
160
161    private synchronized boolean transition(final State target) {
162        final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
163        if (validTransitions != null && validTransitions.contains(target)) {
164            this.state = target;
165            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
166            return true;
167        } else {
168            return false;
169        }
170    }
171
172    private void transitionOrThrow(final State target) {
173        if (!transition(target)) {
174            throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target));
175        }
176    }
177
178    private static class DescriptionTransport {
179        private final RtpDescription description;
180        private final IceUdpTransportInfo transport;
181
182        public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) {
183            this.description = description;
184            this.transport = transport;
185        }
186
187        public static DescriptionTransport of(final Content content) {
188            final GenericDescription description = content.getDescription();
189            final GenericTransportInfo transportInfo = content.getTransport();
190            final RtpDescription rtpDescription;
191            final IceUdpTransportInfo iceUdpTransportInfo;
192            if (description instanceof RtpDescription) {
193                rtpDescription = (RtpDescription) description;
194            } else {
195                throw new IllegalArgumentException("Content does not contain RtpDescription");
196            }
197            if (transportInfo instanceof IceUdpTransportInfo) {
198                iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
199            } else {
200                throw new IllegalArgumentException("Content does not contain ICE-UDP transport");
201            }
202            return new DescriptionTransport(rtpDescription, iceUdpTransportInfo);
203        }
204
205        public static Map<String, DescriptionTransport> of(final Map<String,Content> contents) {
206            return Maps.transformValues(contents, new Function<Content, DescriptionTransport>() {
207                @NullableDecl
208                @Override
209                public DescriptionTransport apply(@NullableDecl Content content) {
210                    return content == null ? null : of(content);
211                }
212            });
213        }
214    }
215
216}