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