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                processContents(contents);
 77                sendSessionAccept();
 78            } else {
 79                //TODO start ringing
 80            }
 81        } else {
 82            Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state));
 83        }
 84    }
 85
 86    private void processContents(final Map<String,DescriptionTransport> contents) {
 87        for(Map.Entry<String,DescriptionTransport> content : contents.entrySet()) {
 88            final DescriptionTransport descriptionTransport = content.getValue();
 89            final RtpDescription rtpDescription = descriptionTransport.description;
 90            Log.d(Config.LOGTAG,"receive content with name "+content.getKey()+" and media="+rtpDescription.getMedia());
 91            for(RtpDescription.PayloadType payloadType : rtpDescription.getPayloadTypes()) {
 92                Log.d(Config.LOGTAG,"payload type: "+payloadType.toString());
 93            }
 94            for(RtpDescription.RtpHeaderExtension extension : rtpDescription.getHeaderExtensions()) {
 95                Log.d(Config.LOGTAG,"extension: "+extension.toString());
 96            }
 97            final IceUdpTransportInfo iceUdpTransportInfo = descriptionTransport.transport;
 98            Log.d(Config.LOGTAG,"transport: "+descriptionTransport.transport);
 99            Log.d(Config.LOGTAG,"fingerprint "+iceUdpTransportInfo.getFingerprint());
100        }
101    }
102
103    void deliveryMessage(final Jid from, final Element message) {
104        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message);
105        switch (message.getName()) {
106            case "propose":
107                receivePropose(from, message);
108                break;
109            case "proceed":
110                receiveProceed(from, message);
111            default:
112                break;
113        }
114    }
115
116    private void receivePropose(final Jid from, final Element propose) {
117        final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid());
118        if (originatedFromMyself) {
119            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring");
120        } else if (transition(State.PROPOSED)) {
121            //TODO start ringing or something
122            pickUpCall();
123        } else {
124            Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state);
125        }
126    }
127
128    private void receiveProceed(final Jid from, final Element proceed) {
129        if (from.equals(id.with)) {
130            if (isInitiator()) {
131                if (transition(State.SESSION_INITIALIZED)) {
132                    this.sendSessionInitiate();
133                } else {
134                    Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state));
135                }
136            } else {
137                Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because we were not initializing", id.account.getJid().asBareJid()));
138            }
139        } else {
140            Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with));
141        }
142    }
143
144    private void sendSessionInitiate() {
145
146    }
147
148    private void sendSessionAccept() {
149        Log.d(Config.LOGTAG,"sending session-accept");
150    }
151
152    public void pickUpCall() {
153        switch (this.state) {
154            case PROPOSED:
155                pickupCallFromProposed();
156                break;
157            case SESSION_INITIALIZED:
158                pickupCallFromSessionInitialized();
159                break;
160            default:
161                throw new IllegalStateException("Can not pick up call from " + this.state);
162        }
163    }
164
165    private void pickupCallFromProposed() {
166        transitionOrThrow(State.PROCEED);
167        final MessagePacket messagePacket = new MessagePacket();
168        messagePacket.setTo(id.with);
169        //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916
170        messagePacket.addChild("accept", Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId);
171        Log.d(Config.LOGTAG, messagePacket.toString());
172        xmppConnectionService.sendMessagePacket(id.account, messagePacket);
173    }
174
175    private void pickupCallFromSessionInitialized() {
176
177    }
178
179    private synchronized boolean transition(final State target) {
180        final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
181        if (validTransitions != null && validTransitions.contains(target)) {
182            this.state = target;
183            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
184            return true;
185        } else {
186            return false;
187        }
188    }
189
190    private void transitionOrThrow(final State target) {
191        if (!transition(target)) {
192            throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target));
193        }
194    }
195
196    public static class DescriptionTransport {
197        private final RtpDescription description;
198        private final IceUdpTransportInfo transport;
199
200        public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) {
201            this.description = description;
202            this.transport = transport;
203        }
204
205        public static DescriptionTransport of(final Content content) {
206            final GenericDescription description = content.getDescription();
207            final GenericTransportInfo transportInfo = content.getTransport();
208            final RtpDescription rtpDescription;
209            final IceUdpTransportInfo iceUdpTransportInfo;
210            if (description instanceof RtpDescription) {
211                rtpDescription = (RtpDescription) description;
212            } else {
213                Log.d(Config.LOGTAG,"description was "+description);
214                throw new IllegalArgumentException("Content does not contain RtpDescription");
215            }
216            if (transportInfo instanceof IceUdpTransportInfo) {
217                iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
218            } else {
219                throw new IllegalArgumentException("Content does not contain ICE-UDP transport");
220            }
221            return new DescriptionTransport(rtpDescription, iceUdpTransportInfo);
222        }
223
224        public static Map<String, DescriptionTransport> of(final Map<String,Content> contents) {
225            return ImmutableMap.copyOf(Maps.transformValues(contents, new Function<Content, DescriptionTransport>() {
226                @NullableDecl
227                @Override
228                public DescriptionTransport apply(@NullableDecl Content content) {
229                    return content == null ? null : of(content);
230                }
231            }));
232        }
233    }
234
235}