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}