1package eu.siacs.conversations.xmpp.jingle;
2
3import android.os.SystemClock;
4import android.util.Base64;
5import android.util.Log;
6
7import com.google.common.base.Function;
8import com.google.common.base.Objects;
9import com.google.common.base.Preconditions;
10import com.google.common.cache.Cache;
11import com.google.common.cache.CacheBuilder;
12import com.google.common.collect.Collections2;
13import com.google.common.collect.ImmutableSet;
14
15import org.checkerframework.checker.nullness.compatqual.NullableDecl;
16
17import java.lang.ref.WeakReference;
18import java.security.SecureRandom;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.HashMap;
22import java.util.List;
23import java.util.Map;
24import java.util.Set;
25import java.util.concurrent.ConcurrentHashMap;
26import java.util.concurrent.TimeUnit;
27
28import eu.siacs.conversations.Config;
29import eu.siacs.conversations.entities.Account;
30import eu.siacs.conversations.entities.Contact;
31import eu.siacs.conversations.entities.Conversation;
32import eu.siacs.conversations.entities.Conversational;
33import eu.siacs.conversations.entities.Message;
34import eu.siacs.conversations.entities.RtpSessionStatus;
35import eu.siacs.conversations.entities.Transferable;
36import eu.siacs.conversations.services.AbstractConnectionManager;
37import eu.siacs.conversations.services.XmppConnectionService;
38import eu.siacs.conversations.xml.Element;
39import eu.siacs.conversations.xml.Namespace;
40import eu.siacs.conversations.xmpp.OnIqPacketReceived;
41import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
42import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
43import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
44import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
45import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
46import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
47import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
48import eu.siacs.conversations.xmpp.stanzas.IqPacket;
49import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
50import rocks.xmpp.addr.Jid;
51
52public class JingleConnectionManager extends AbstractConnectionManager {
53 private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals = new HashMap<>();
54 private final Map<AbstractJingleConnection.Id, AbstractJingleConnection> connections = new ConcurrentHashMap<>();
55
56 private final Cache<PersistableSessionId, JingleRtpConnection.State> endedSessions = CacheBuilder.newBuilder()
57 .expireAfterWrite(30, TimeUnit.MINUTES)
58 .build();
59
60 private HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
61
62 public JingleConnectionManager(XmppConnectionService service) {
63 super(service);
64 }
65
66 static String nextRandomId() {
67 final byte[] id = new byte[16];
68 new SecureRandom().nextBytes(id);
69 return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING);
70 }
71
72 public void deliverPacket(final Account account, final JinglePacket packet) {
73 final String sessionId = packet.getSessionId();
74 if (sessionId == null) {
75 respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
76 return;
77 }
78 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet);
79 final AbstractJingleConnection existingJingleConnection = connections.get(id);
80 if (existingJingleConnection != null) {
81 existingJingleConnection.deliverPacket(packet);
82 } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) {
83 final Jid from = packet.getFrom();
84 final Content content = packet.getJingleContent();
85 final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace();
86 final AbstractJingleConnection connection;
87 if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
88 connection = new JingleFileTransferConnection(this, id, from);
89 } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) && !usesTor(account)) {
90 final boolean sessionEnded = this.endedSessions.asMap().containsKey(PersistableSessionId.of(id));
91 final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
92 if (isBusy() || sessionEnded || stranger) {
93 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejected session with " + id.with + " because busy. sessionEnded=" + sessionEnded+", stranger="+stranger);
94 mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null);
95 final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
96 sessionTermination.setTo(id.with);
97 sessionTermination.setReason(Reason.BUSY, null);
98 mXmppConnectionService.sendIqPacket(account, sessionTermination, null);
99 return;
100 }
101 connection = new JingleRtpConnection(this, id, from);
102 } else {
103 respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel");
104 return;
105 }
106 connections.put(id, connection);
107 connection.deliverPacket(packet);
108 } else {
109 Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet);
110 respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
111 }
112 }
113
114 private boolean usesTor(final Account account) {
115 return account.isOnion() || mXmppConnectionService.useTorToConnect();
116 }
117
118 public boolean isBusy() {
119 for (AbstractJingleConnection connection : this.connections.values()) {
120 if (connection instanceof JingleRtpConnection) {
121 return true;
122 }
123 }
124 synchronized (this.rtpSessionProposals) {
125 return this.rtpSessionProposals.containsValue(DeviceDiscoveryState.DISCOVERED) || this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING);
126 }
127 }
128
129 private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) {
130 final boolean notifyForStrangers = mXmppConnectionService.getNotificationService().notificationsFromStrangers();
131 if (notifyForStrangers) {
132 return false;
133 }
134 final Contact contact = account.getRoster().getContact(with);
135 return !contact.showInContactList();
136 }
137
138 public void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) {
139 final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR);
140 final Element error = response.addChild("error");
141 error.setAttribute("type", conditionType);
142 error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
143 error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1");
144 account.getXmppConnection().sendIqPacket(response, null);
145 }
146
147 public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message, String serverMsgId, long timestamp) {
148 Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace()));
149 final String sessionId = message.getAttribute("id");
150 if (sessionId == null) {
151 return;
152 }
153 if ("accept".equals(message.getName())) {
154 for (AbstractJingleConnection connection : connections.values()) {
155 if (connection instanceof JingleRtpConnection) {
156 final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
157 final AbstractJingleConnection.Id id = connection.getId();
158 if (id.account == account && id.sessionId.equals(sessionId)) {
159 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
160 return;
161 }
162 }
163 }
164 return;
165 }
166 final boolean fromSelf = from.asBareJid().equals(account.getJid().asBareJid());
167 final AbstractJingleConnection.Id id;
168 if (fromSelf) {
169 if (to.isFullJid()) {
170 id = AbstractJingleConnection.Id.of(account, to, sessionId);
171 } else {
172 return;
173 }
174 } else {
175 id = AbstractJingleConnection.Id.of(account, from, sessionId);
176 }
177 final AbstractJingleConnection existingJingleConnection = connections.get(id);
178 if (existingJingleConnection != null) {
179 if (existingJingleConnection instanceof JingleRtpConnection) {
180 ((JingleRtpConnection) existingJingleConnection).deliveryMessage(from, message, serverMsgId, timestamp);
181 } else {
182 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + existingJingleConnection.getClass().getName() + " does not support jingle messages");
183 }
184 return;
185 }
186
187 if (fromSelf) {
188 if ("proceed".equals(message.getName())) {
189 final Conversation c = mXmppConnectionService.findOrCreateConversation(account, id.with, false, false);
190 final Message previousBusy = c.findRtpSession(sessionId, Message.STATUS_RECEIVED);
191 if (previousBusy != null) {
192 previousBusy.setBody(new RtpSessionStatus(true, 0).toString());
193 if (serverMsgId != null) {
194 previousBusy.setServerMsgId(serverMsgId);
195 }
196 previousBusy.setTime(timestamp);
197 mXmppConnectionService.updateMessage(previousBusy, true);
198 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": updated previous busy because call got picked up by another device");
199 return;
200 }
201 }
202 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self");
203 return;
204 }
205
206 if ("propose".equals(message.getName())) {
207 final Propose propose = Propose.upgrade(message);
208 final List<GenericDescription> descriptions = propose.getDescriptions();
209 final Collection<RtpDescription> rtpDescriptions = Collections2.transform(
210 Collections2.filter(descriptions, d -> d instanceof RtpDescription),
211 input -> (RtpDescription) input
212 );
213 if (rtpDescriptions.size() > 0 && rtpDescriptions.size() == descriptions.size() && !usesTor(account)) {
214 final Collection<Media> media = Collections2.transform(rtpDescriptions, RtpDescription::getMedia);
215 if (media.contains(Media.UNKNOWN)) {
216 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered unknown media in session proposal. " + propose);
217 return;
218 }
219 final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
220 if (isBusy() || stranger) {
221 writeLogMissedIncoming(account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp);
222 if (stranger) {
223 Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": ignoring call proposal from stranger "+id.with);
224 return;
225 }
226 final int activeDevices = account.countPresences();
227 Log.d(Config.LOGTAG, "active devices: " + activeDevices);
228 if (activeDevices == 0) {
229 final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId);
230 mXmppConnectionService.sendMessagePacket(account, reject);
231 } else {
232 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring proposal because busy on this device but there are other devices");
233 }
234 } else {
235 final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from);
236 this.connections.put(id, rtpConnection);
237 rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
238 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
239 }
240 } else {
241 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed session with " + rtpDescriptions.size() + " rtp descriptions of " + descriptions.size() + " total descriptions");
242 }
243 } else if ("proceed".equals(message.getName())) {
244 synchronized (rtpSessionProposals) {
245 final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId);
246 if (proposal != null) {
247 rtpSessionProposals.remove(proposal);
248 final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid());
249 rtpConnection.setProposedMedia(proposal.media);
250 this.connections.put(id, rtpConnection);
251 rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED);
252 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
253 } else {
254 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver proceed");
255 }
256 }
257 } else if ("reject".equals(message.getName())) {
258 final RtpSessionProposal proposal = new RtpSessionProposal(account, from.asBareJid(), sessionId);
259 synchronized (rtpSessionProposals) {
260 if (rtpSessionProposals.remove(proposal) != null) {
261 writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
262 mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY);
263 } else {
264 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject");
265 }
266 }
267 } else {
268 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved out of order jingle message");
269 }
270
271 }
272
273 private RtpSessionProposal getRtpSessionProposal(final Account account, Jid from, String sessionId) {
274 for (RtpSessionProposal rtpSessionProposal : rtpSessionProposals.keySet()) {
275 if (rtpSessionProposal.sessionId.equals(sessionId) && rtpSessionProposal.with.equals(from) && rtpSessionProposal.account.getJid().equals(account.getJid())) {
276 return rtpSessionProposal;
277 }
278 }
279 return null;
280 }
281
282 private void writeLogMissedOutgoing(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) {
283 final Conversation conversation = mXmppConnectionService.findOrCreateConversation(
284 account,
285 with.asBareJid(),
286 false,
287 false
288 );
289 final Message message = new Message(
290 conversation,
291 Message.STATUS_SEND,
292 Message.TYPE_RTP_SESSION,
293 sessionId
294 );
295 message.setBody(new RtpSessionStatus(false, 0).toString());
296 message.setServerMsgId(serverMsgId);
297 message.setTime(timestamp);
298 writeMessage(message);
299 }
300
301 private void writeLogMissedIncoming(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) {
302 final Conversation conversation = mXmppConnectionService.findOrCreateConversation(
303 account,
304 with.asBareJid(),
305 false,
306 false
307 );
308 final Message message = new Message(
309 conversation,
310 Message.STATUS_RECEIVED,
311 Message.TYPE_RTP_SESSION,
312 sessionId
313 );
314 message.setBody(new RtpSessionStatus(false, 0).toString());
315 message.setServerMsgId(serverMsgId);
316 message.setTime(timestamp);
317 writeMessage(message);
318 }
319
320 private void writeMessage(final Message message) {
321 final Conversational conversational = message.getConversation();
322 if (conversational instanceof Conversation) {
323 ((Conversation) conversational).add(message);
324 mXmppConnectionService.databaseBackend.createMessage(message);
325 mXmppConnectionService.updateConversationUi();
326 } else {
327 throw new IllegalStateException("Somehow the conversation in a message was a stub");
328 }
329 }
330
331 public void startJingleFileTransfer(final Message message) {
332 Preconditions.checkArgument(message.isFileOrImage(), "Message is not of type file or image");
333 final Transferable old = message.getTransferable();
334 if (old != null) {
335 old.cancel();
336 }
337 final Account account = message.getConversation().getAccount();
338 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message);
339 final JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id, account.getJid());
340 mXmppConnectionService.markMessage(message, Message.STATUS_WAITING);
341 this.connections.put(id, connection);
342 connection.init(message);
343 }
344
345 void finishConnection(final AbstractJingleConnection connection) {
346 this.connections.remove(connection.getId());
347 }
348
349 void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) {
350 if (Config.DISABLE_PROXY_LOOKUP) {
351 listener.onPrimaryCandidateFound(false, null);
352 return;
353 }
354 if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) {
355 final Jid proxy = account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS);
356 if (proxy != null) {
357 IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
358 iq.setTo(proxy);
359 iq.query(Namespace.BYTE_STREAMS);
360 account.getXmppConnection().sendIqPacket(iq, new OnIqPacketReceived() {
361
362 @Override
363 public void onIqPacketReceived(Account account, IqPacket packet) {
364 final Element streamhost = packet.query().findChild("streamhost", Namespace.BYTE_STREAMS);
365 final String host = streamhost == null ? null : streamhost.getAttribute("host");
366 final String port = streamhost == null ? null : streamhost.getAttribute("port");
367 if (host != null && port != null) {
368 try {
369 JingleCandidate candidate = new JingleCandidate(nextRandomId(), true);
370 candidate.setHost(host);
371 candidate.setPort(Integer.parseInt(port));
372 candidate.setType(JingleCandidate.TYPE_PROXY);
373 candidate.setJid(proxy);
374 candidate.setPriority(655360 + (initiator ? 30 : 0));
375 primaryCandidates.put(account.getJid().asBareJid(), candidate);
376 listener.onPrimaryCandidateFound(true, candidate);
377 } catch (final NumberFormatException e) {
378 listener.onPrimaryCandidateFound(false, null);
379 }
380 } else {
381 listener.onPrimaryCandidateFound(false, null);
382 }
383 }
384 });
385 } else {
386 listener.onPrimaryCandidateFound(false, null);
387 }
388
389 } else {
390 listener.onPrimaryCandidateFound(true,
391 this.primaryCandidates.get(account.getJid().asBareJid()));
392 }
393 }
394
395 public void retractSessionProposal(final Account account, final Jid with) {
396 synchronized (this.rtpSessionProposals) {
397 RtpSessionProposal matchingProposal = null;
398 for (RtpSessionProposal proposal : this.rtpSessionProposals.keySet()) {
399 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
400 matchingProposal = proposal;
401 break;
402 }
403 }
404 if (matchingProposal != null) {
405 this.rtpSessionProposals.remove(matchingProposal);
406 final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(matchingProposal);
407 writeLogMissedOutgoing(account, matchingProposal.with, matchingProposal.sessionId, null, System.currentTimeMillis());
408 mXmppConnectionService.sendMessagePacket(account, messagePacket);
409
410 }
411 }
412 }
413
414 public void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
415 synchronized (this.rtpSessionProposals) {
416 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
417 RtpSessionProposal proposal = entry.getKey();
418 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
419 final DeviceDiscoveryState preexistingState = entry.getValue();
420 if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) {
421 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
422 account,
423 with,
424 proposal.sessionId,
425 preexistingState.toEndUserState()
426 );
427 return;
428 }
429 }
430 }
431 if (isBusy()) {
432 throw new IllegalStateException("There is already a running RTP session. This should have been caught by the UI");
433 }
434 final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media);
435 this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
436 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
437 account,
438 proposal.with,
439 proposal.sessionId,
440 RtpEndUserState.FINDING_DEVICE
441 );
442 final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
443 Log.d(Config.LOGTAG, messagePacket.toString());
444 mXmppConnectionService.sendMessagePacket(account, messagePacket);
445 }
446 }
447
448 public void deliverIbbPacket(Account account, IqPacket packet) {
449 final String sid;
450 final Element payload;
451 if (packet.hasChild("open", Namespace.IBB)) {
452 payload = packet.findChild("open", Namespace.IBB);
453 sid = payload.getAttribute("sid");
454 } else if (packet.hasChild("data", Namespace.IBB)) {
455 payload = packet.findChild("data", Namespace.IBB);
456 sid = payload.getAttribute("sid");
457 } else if (packet.hasChild("close", Namespace.IBB)) {
458 payload = packet.findChild("close", Namespace.IBB);
459 sid = payload.getAttribute("sid");
460 } else {
461 payload = null;
462 sid = null;
463 }
464 if (sid != null) {
465 for (final AbstractJingleConnection connection : this.connections.values()) {
466 if (connection instanceof JingleFileTransferConnection) {
467 final JingleFileTransferConnection fileTransfer = (JingleFileTransferConnection) connection;
468 final JingleTransport transport = fileTransfer.getTransport();
469 if (transport instanceof JingleInBandTransport) {
470 final JingleInBandTransport inBandTransport = (JingleInBandTransport) transport;
471 if (inBandTransport.matches(account, sid)) {
472 inBandTransport.deliverPayload(packet, payload);
473 }
474 return;
475 }
476 }
477 }
478 }
479 Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString());
480 account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
481 }
482
483 public void notifyRebound() {
484 for (final AbstractJingleConnection connection : this.connections.values()) {
485 connection.notifyRebound();
486 }
487 }
488
489 public WeakReference<JingleRtpConnection> findJingleRtpConnection(Account account, Jid with, String sessionId) {
490 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, Jid.ofEscaped(with), sessionId);
491 final AbstractJingleConnection connection = connections.get(id);
492 if (connection instanceof JingleRtpConnection) {
493 return new WeakReference<>((JingleRtpConnection) connection);
494 }
495 return null;
496 }
497
498 public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) {
499 synchronized (this.rtpSessionProposals) {
500 final RtpSessionProposal sessionProposal = getRtpSessionProposal(account, from.asBareJid(), sessionId);
501 final DeviceDiscoveryState currentState = sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal);
502 if (currentState == null) {
503 Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId);
504 return;
505 }
506 if (currentState == DeviceDiscoveryState.DISCOVERED) {
507 Log.d(Config.LOGTAG, "session proposal already at discovered. not going to fall back");
508 return;
509 }
510 this.rtpSessionProposals.put(sessionProposal, target);
511 mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, target.toEndUserState());
512 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target);
513 }
514 }
515
516 public void rejectRtpSession(final String sessionId) {
517 for (final AbstractJingleConnection connection : this.connections.values()) {
518 if (connection.getId().sessionId.equals(sessionId)) {
519 if (connection instanceof JingleRtpConnection) {
520 ((JingleRtpConnection) connection).rejectCall();
521 }
522 }
523 }
524 }
525
526 public void endRtpSession(final String sessionId) {
527 for (final AbstractJingleConnection connection : this.connections.values()) {
528 if (connection.getId().sessionId.equals(sessionId)) {
529 if (connection instanceof JingleRtpConnection) {
530 ((JingleRtpConnection) connection).endCall();
531 }
532 }
533 }
534 }
535
536 public void failProceed(Account account, final Jid with, String sessionId) {
537 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId);
538 final AbstractJingleConnection existingJingleConnection = connections.get(id);
539 if (existingJingleConnection instanceof JingleRtpConnection) {
540 ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed();
541 }
542 }
543
544 void ensureConnectionIsRegistered(final AbstractJingleConnection connection) {
545 if (connections.containsValue(connection)) {
546 return;
547 }
548 throw new IllegalStateException("JingleConnection has not been registered with connection manager");
549 }
550
551 public void endSession(AbstractJingleConnection.Id id, final AbstractJingleConnection.State state) {
552 this.endedSessions.put(PersistableSessionId.of(id), state);
553 }
554
555 private static class PersistableSessionId {
556 private final Jid with;
557 private final String sessionId;
558
559 private PersistableSessionId(Jid with, String sessionId) {
560 this.with = with;
561 this.sessionId = sessionId;
562 }
563
564 public static PersistableSessionId of(AbstractJingleConnection.Id id) {
565 return new PersistableSessionId(id.with, id.sessionId);
566 }
567
568 @Override
569 public boolean equals(Object o) {
570 if (this == o) return true;
571 if (o == null || getClass() != o.getClass()) return false;
572 PersistableSessionId that = (PersistableSessionId) o;
573 return Objects.equal(with, that.with) &&
574 Objects.equal(sessionId, that.sessionId);
575 }
576
577 @Override
578 public int hashCode() {
579 return Objects.hashCode(with, sessionId);
580 }
581 }
582
583 public enum DeviceDiscoveryState {
584 SEARCHING, DISCOVERED, FAILED;
585
586 public RtpEndUserState toEndUserState() {
587 switch (this) {
588 case SEARCHING:
589 return RtpEndUserState.FINDING_DEVICE;
590 case DISCOVERED:
591 return RtpEndUserState.RINGING;
592 default:
593 return RtpEndUserState.CONNECTIVITY_ERROR;
594 }
595 }
596 }
597
598 public static class RtpSessionProposal {
599 public final Jid with;
600 public final String sessionId;
601 public final Set<Media> media;
602 private final Account account;
603
604 private RtpSessionProposal(Account account, Jid with, String sessionId) {
605 this(account, with, sessionId, Collections.emptySet());
606 }
607
608 private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media) {
609 this.account = account;
610 this.with = with;
611 this.sessionId = sessionId;
612 this.media = media;
613 }
614
615 public static RtpSessionProposal of(Account account, Jid with, Set<Media> media) {
616 return new RtpSessionProposal(account, with, nextRandomId(), media);
617 }
618
619 @Override
620 public boolean equals(Object o) {
621 if (this == o) return true;
622 if (o == null || getClass() != o.getClass()) return false;
623 RtpSessionProposal proposal = (RtpSessionProposal) o;
624 return Objects.equal(account.getJid(), proposal.account.getJid()) &&
625 Objects.equal(with, proposal.with) &&
626 Objects.equal(sessionId, proposal.sessionId);
627 }
628
629 @Override
630 public int hashCode() {
631 return Objects.hashCode(account.getJid(), with, sessionId);
632 }
633 }
634}