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}