1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Base64;
4import android.util.Log;
5
6import com.google.common.base.Objects;
7import com.google.common.base.Optional;
8import com.google.common.base.Preconditions;
9import com.google.common.cache.Cache;
10import com.google.common.cache.CacheBuilder;
11import com.google.common.collect.Collections2;
12import com.google.common.collect.ComparisonChain;
13import com.google.common.collect.ImmutableSet;
14
15import java.lang.ref.WeakReference;
16import java.security.SecureRandom;
17import java.util.Arrays;
18import java.util.Collection;
19import java.util.Collections;
20import java.util.HashMap;
21import java.util.List;
22import java.util.Map;
23import java.util.Set;
24import java.util.concurrent.ConcurrentHashMap;
25import java.util.concurrent.Executors;
26import java.util.concurrent.ScheduledExecutorService;
27import java.util.concurrent.ScheduledFuture;
28import java.util.concurrent.TimeUnit;
29
30import eu.siacs.conversations.Config;
31import eu.siacs.conversations.entities.Account;
32import eu.siacs.conversations.entities.Contact;
33import eu.siacs.conversations.entities.Conversation;
34import eu.siacs.conversations.entities.Conversational;
35import eu.siacs.conversations.entities.Message;
36import eu.siacs.conversations.entities.RtpSessionStatus;
37import eu.siacs.conversations.entities.Transferable;
38import eu.siacs.conversations.services.AbstractConnectionManager;
39import eu.siacs.conversations.services.XmppConnectionService;
40import eu.siacs.conversations.xml.Element;
41import eu.siacs.conversations.xml.Namespace;
42import eu.siacs.conversations.xmpp.Jid;
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;
53
54public class JingleConnectionManager extends AbstractConnectionManager {
55 static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();
56 final ToneManager toneManager;
57 private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals = new HashMap<>();
58 private final ConcurrentHashMap<AbstractJingleConnection.Id, AbstractJingleConnection> connections = new ConcurrentHashMap<>();
59
60 private final Cache<PersistableSessionId, TerminatedRtpSession> terminatedSessions = CacheBuilder.newBuilder()
61 .expireAfterWrite(24, TimeUnit.HOURS)
62 .build();
63
64 private HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
65
66 public JingleConnectionManager(XmppConnectionService service) {
67 super(service);
68 this.toneManager = new ToneManager(service);
69 }
70
71 static String nextRandomId() {
72 final byte[] id = new byte[16];
73 new SecureRandom().nextBytes(id);
74 return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING);
75 }
76
77 public void deliverPacket(final Account account, final JinglePacket packet) {
78 final String sessionId = packet.getSessionId();
79 if (sessionId == null) {
80 respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
81 return;
82 }
83 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet);
84 final AbstractJingleConnection existingJingleConnection = connections.get(id);
85 if (existingJingleConnection != null) {
86 existingJingleConnection.deliverPacket(packet);
87 } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) {
88 final Jid from = packet.getFrom();
89 final Content content = packet.getJingleContent();
90 final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace();
91 final AbstractJingleConnection connection;
92 if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
93 connection = new JingleFileTransferConnection(this, id, from);
94 } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) && isUsingClearNet(account)) {
95 final boolean sessionEnded = this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id));
96 final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
97 if (isBusy() || sessionEnded || stranger) {
98 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejected session with " + id.with + " because busy. sessionEnded=" + sessionEnded + ", stranger=" + stranger);
99 mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null);
100 final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
101 sessionTermination.setTo(id.with);
102 sessionTermination.setReason(Reason.BUSY, null);
103 mXmppConnectionService.sendIqPacket(account, sessionTermination, null);
104 return;
105 }
106 connection = new JingleRtpConnection(this, id, from);
107 } else {
108 respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel");
109 return;
110 }
111 connections.put(id, connection);
112 mXmppConnectionService.updateConversationUi();
113 connection.deliverPacket(packet);
114 } else {
115 Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet);
116 respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
117 }
118 }
119
120 private boolean isUsingClearNet(final Account account) {
121 return !account.isOnion() && !mXmppConnectionService.useTorToConnect();
122 }
123
124 public boolean isBusy() {
125 if (mXmppConnectionService.isPhoneInCall()) {
126 return true;
127 }
128 for (AbstractJingleConnection connection : this.connections.values()) {
129 if (connection instanceof JingleRtpConnection) {
130 if (((JingleRtpConnection) connection).isTerminated()) {
131 continue;
132 }
133 return true;
134 }
135 }
136 synchronized (this.rtpSessionProposals) {
137 return this.rtpSessionProposals.containsValue(DeviceDiscoveryState.DISCOVERED) || this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING);
138 }
139 }
140
141 public void notifyPhoneCallStarted() {
142 for (AbstractJingleConnection connection : connections.values()) {
143 if (connection instanceof JingleRtpConnection) {
144 final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
145 if (rtpConnection.isTerminated()) {
146 continue;
147 }
148 rtpConnection.notifyPhoneCall();
149 }
150 }
151 }
152
153 private Optional<RtpSessionProposal> findMatchingSessionProposal(final Account account, final Jid with, final Set<Media> media) {
154 synchronized (this.rtpSessionProposals) {
155 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
156 final RtpSessionProposal proposal = entry.getKey();
157 final DeviceDiscoveryState state = entry.getValue();
158 final boolean openProposal = state == DeviceDiscoveryState.DISCOVERED || state == DeviceDiscoveryState.SEARCHING;
159 if (openProposal
160 && proposal.account == account
161 && proposal.with.equals(with.asBareJid())
162 && proposal.media.equals(media)) {
163 return Optional.of(proposal);
164 }
165 }
166 }
167 return Optional.absent();
168 }
169
170 private boolean hasMatchingRtpSession(final Account account, final Jid with, final Set<Media> media) {
171 for (AbstractJingleConnection connection : this.connections.values()) {
172 if (connection instanceof JingleRtpConnection) {
173 final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
174 if (rtpConnection.isTerminated()) {
175 continue;
176 }
177 if (rtpConnection.getId().account == account
178 && rtpConnection.getId().with.asBareJid().equals(with.asBareJid())
179 && rtpConnection.getMedia().equals(media)) {
180 return true;
181 }
182 }
183 }
184 return false;
185 }
186
187 private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) {
188 final boolean notifyForStrangers = mXmppConnectionService.getNotificationService().notificationsFromStrangers();
189 if (notifyForStrangers) {
190 return false;
191 }
192 final Contact contact = account.getRoster().getContact(with);
193 return !contact.showInContactList();
194 }
195
196 ScheduledFuture<?> schedule(final Runnable runnable, final long delay, final TimeUnit timeUnit) {
197 return SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, timeUnit);
198 }
199
200 void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) {
201 final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR);
202 final Element error = response.addChild("error");
203 error.setAttribute("type", conditionType);
204 error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
205 error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1");
206 account.getXmppConnection().sendIqPacket(response, null);
207 }
208
209 public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message, String remoteMsgId, String serverMsgId, long timestamp) {
210 Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace()));
211 final String sessionId = message.getAttribute("id");
212 if (sessionId == null) {
213 return;
214 }
215 if ("accept".equals(message.getName())) {
216 for (AbstractJingleConnection connection : connections.values()) {
217 if (connection instanceof JingleRtpConnection) {
218 final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
219 final AbstractJingleConnection.Id id = connection.getId();
220 if (id.account == account && id.sessionId.equals(sessionId)) {
221 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
222 return;
223 }
224 }
225 }
226 return;
227 }
228 final boolean fromSelf = from.asBareJid().equals(account.getJid().asBareJid());
229 final boolean addressedDirectly = to != null && to.equals(account.getJid());
230 final AbstractJingleConnection.Id id;
231 if (fromSelf) {
232 if (to != null && to.isFullJid()) {
233 id = AbstractJingleConnection.Id.of(account, to, sessionId);
234 } else {
235 return;
236 }
237 } else {
238 id = AbstractJingleConnection.Id.of(account, from, sessionId);
239 }
240 final AbstractJingleConnection existingJingleConnection = connections.get(id);
241 if (existingJingleConnection != null) {
242 if (existingJingleConnection instanceof JingleRtpConnection) {
243 ((JingleRtpConnection) existingJingleConnection).deliveryMessage(from, message, serverMsgId, timestamp);
244 } else {
245 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + existingJingleConnection.getClass().getName() + " does not support jingle messages");
246 }
247 return;
248 }
249
250 if (fromSelf) {
251 if ("proceed".equals(message.getName())) {
252 final Conversation c = mXmppConnectionService.findOrCreateConversation(account, id.with, false, false);
253 final Message previousBusy = c.findRtpSession(sessionId, Message.STATUS_RECEIVED);
254 if (previousBusy != null) {
255 previousBusy.setBody(new RtpSessionStatus(true, 0).toString());
256 if (serverMsgId != null) {
257 previousBusy.setServerMsgId(serverMsgId);
258 }
259 previousBusy.setTime(timestamp);
260 mXmppConnectionService.updateMessage(previousBusy, true);
261 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": updated previous busy because call got picked up by another device");
262 return;
263 }
264 }
265 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self");
266 return;
267 }
268
269 if ("propose".equals(message.getName())) {
270 final Propose propose = Propose.upgrade(message);
271 final List<GenericDescription> descriptions = propose.getDescriptions();
272 final Collection<RtpDescription> rtpDescriptions = Collections2.transform(
273 Collections2.filter(descriptions, d -> d instanceof RtpDescription),
274 input -> (RtpDescription) input
275 );
276 if (rtpDescriptions.size() > 0 && rtpDescriptions.size() == descriptions.size() && isUsingClearNet(account)) {
277 final Collection<Media> media = Collections2.transform(rtpDescriptions, RtpDescription::getMedia);
278 if (media.contains(Media.UNKNOWN)) {
279 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered unknown media in session proposal. " + propose);
280 return;
281 }
282 final Optional<RtpSessionProposal> matchingSessionProposal = findMatchingSessionProposal(account, id.with, ImmutableSet.copyOf(media));
283 if (matchingSessionProposal.isPresent()) {
284 final String ourSessionId = matchingSessionProposal.get().sessionId;
285 final String theirSessionId = id.sessionId;
286 if (ComparisonChain.start()
287 .compare(ourSessionId, theirSessionId)
288 .compare(account.getJid().toEscapedString(), id.with.toEscapedString())
289 .result() > 0) {
290 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": our session lost tie break. automatically accepting their session. winning Session=" + theirSessionId);
291 //TODO a retract for this reason should probably include some indication of tie break
292 retractSessionProposal(matchingSessionProposal.get());
293 final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from);
294 this.connections.put(id, rtpConnection);
295 rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
296 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
297 } else {
298 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": our session won tie break. waiting for other party to accept. winningSession=" + ourSessionId);
299 }
300 return;
301 }
302 final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
303 if (isBusy() || stranger) {
304 writeLogMissedIncoming(account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp);
305 if (stranger) {
306 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring call proposal from stranger " + id.with);
307 return;
308 }
309 final int activeDevices = account.activeDevicesWithRtpCapability();
310 Log.d(Config.LOGTAG, "active devices with rtp capability: " + activeDevices);
311 if (activeDevices == 0) {
312 final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId);
313 mXmppConnectionService.sendMessagePacket(account, reject);
314 } else {
315 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring proposal because busy on this device but there are other devices");
316 }
317 } else {
318 final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from);
319 this.connections.put(id, rtpConnection);
320 rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
321 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
322 }
323 } else {
324 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed session with " + rtpDescriptions.size() + " rtp descriptions of " + descriptions.size() + " total descriptions");
325 }
326 } else if (addressedDirectly && "proceed".equals(message.getName())) {
327 synchronized (rtpSessionProposals) {
328 final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId);
329 if (proposal != null) {
330 rtpSessionProposals.remove(proposal);
331 final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid());
332 rtpConnection.setProposedMedia(proposal.media);
333 this.connections.put(id, rtpConnection);
334 rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED);
335 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
336 } else {
337 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver proceed");
338 if (remoteMsgId == null) {
339 return;
340 }
341 final MessagePacket errorMessage = new MessagePacket();
342 errorMessage.setTo(from);
343 errorMessage.setId(remoteMsgId);
344 errorMessage.setType(MessagePacket.TYPE_ERROR);
345 final Element error = errorMessage.addChild("error");
346 error.setAttribute("code", "404");
347 error.setAttribute("type", "cancel");
348 error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
349 mXmppConnectionService.sendMessagePacket(account, errorMessage);
350 }
351 }
352 } else if (addressedDirectly && "reject".equals(message.getName())) {
353 final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId);
354 synchronized (rtpSessionProposals) {
355 if (proposal != null && rtpSessionProposals.remove(proposal) != null) {
356 writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
357 toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media);
358 mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY);
359 } else {
360 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject");
361 }
362 }
363 } else {
364 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved out of order jingle message"+message);
365 }
366
367 }
368
369 private RtpSessionProposal getRtpSessionProposal(final Account account, Jid from, String sessionId) {
370 for (RtpSessionProposal rtpSessionProposal : rtpSessionProposals.keySet()) {
371 if (rtpSessionProposal.sessionId.equals(sessionId) && rtpSessionProposal.with.equals(from) && rtpSessionProposal.account.getJid().equals(account.getJid())) {
372 return rtpSessionProposal;
373 }
374 }
375 return null;
376 }
377
378 private void writeLogMissedOutgoing(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) {
379 final Conversation conversation = mXmppConnectionService.findOrCreateConversation(
380 account,
381 with.asBareJid(),
382 false,
383 false
384 );
385 final Message message = new Message(
386 conversation,
387 Message.STATUS_SEND,
388 Message.TYPE_RTP_SESSION,
389 sessionId
390 );
391 message.setBody(new RtpSessionStatus(false, 0).toString());
392 message.setServerMsgId(serverMsgId);
393 message.setTime(timestamp);
394 writeMessage(message);
395 }
396
397 private void writeLogMissedIncoming(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) {
398 final Conversation conversation = mXmppConnectionService.findOrCreateConversation(
399 account,
400 with.asBareJid(),
401 false,
402 false
403 );
404 final Message message = new Message(
405 conversation,
406 Message.STATUS_RECEIVED,
407 Message.TYPE_RTP_SESSION,
408 sessionId
409 );
410 message.setBody(new RtpSessionStatus(false, 0).toString());
411 message.setServerMsgId(serverMsgId);
412 message.setTime(timestamp);
413 writeMessage(message);
414 }
415
416 private void writeMessage(final Message message) {
417 final Conversational conversational = message.getConversation();
418 if (conversational instanceof Conversation) {
419 ((Conversation) conversational).add(message);
420 mXmppConnectionService.databaseBackend.createMessage(message);
421 mXmppConnectionService.updateConversationUi();
422 } else {
423 throw new IllegalStateException("Somehow the conversation in a message was a stub");
424 }
425 }
426
427 public void startJingleFileTransfer(final Message message) {
428 Preconditions.checkArgument(message.isFileOrImage(), "Message is not of type file or image");
429 final Transferable old = message.getTransferable();
430 if (old != null) {
431 old.cancel();
432 }
433 final Account account = message.getConversation().getAccount();
434 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message);
435 final JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id, account.getJid());
436 mXmppConnectionService.markMessage(message, Message.STATUS_WAITING);
437 this.connections.put(id, connection);
438 connection.init(message);
439 }
440
441 public Optional<OngoingRtpSession> getOngoingRtpConnection(final Contact contact) {
442 for (final Map.Entry<AbstractJingleConnection.Id, AbstractJingleConnection> entry : this.connections.entrySet()) {
443 if (entry.getValue() instanceof JingleRtpConnection) {
444 final AbstractJingleConnection.Id id = entry.getKey();
445 if (id.account == contact.getAccount() && id.with.asBareJid().equals(contact.getJid().asBareJid())) {
446 return Optional.of(id);
447 }
448 }
449 }
450 synchronized (this.rtpSessionProposals) {
451 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
452 RtpSessionProposal proposal = entry.getKey();
453 if (proposal.account == contact.getAccount() && contact.getJid().asBareJid().equals(proposal.with)) {
454 final DeviceDiscoveryState preexistingState = entry.getValue();
455 if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) {
456 return Optional.of(proposal);
457 }
458 }
459 }
460 }
461 return Optional.absent();
462 }
463
464 void finishConnection(final AbstractJingleConnection connection) {
465 this.connections.remove(connection.getId());
466 }
467
468 void finishConnectionOrThrow(final AbstractJingleConnection connection) {
469 final AbstractJingleConnection.Id id = connection.getId();
470 if (this.connections.remove(id) == null) {
471 throw new IllegalStateException(String.format("Unable to finish connection with id=%s", id.toString()));
472 }
473 }
474
475 public boolean fireJingleRtpConnectionStateUpdates() {
476 boolean firedUpdates = false;
477 for (final AbstractJingleConnection connection : this.connections.values()) {
478 if (connection instanceof JingleRtpConnection) {
479 final JingleRtpConnection jingleRtpConnection = (JingleRtpConnection) connection;
480 if (jingleRtpConnection.isTerminated()) {
481 continue;
482 }
483 jingleRtpConnection.fireStateUpdate();
484 firedUpdates = true;
485 }
486 }
487 return firedUpdates;
488 }
489
490 void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) {
491 if (Config.DISABLE_PROXY_LOOKUP) {
492 listener.onPrimaryCandidateFound(false, null);
493 return;
494 }
495 if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) {
496 final Jid proxy = account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS);
497 if (proxy != null) {
498 IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
499 iq.setTo(proxy);
500 iq.query(Namespace.BYTE_STREAMS);
501 account.getXmppConnection().sendIqPacket(iq, new OnIqPacketReceived() {
502
503 @Override
504 public void onIqPacketReceived(Account account, IqPacket packet) {
505 final Element streamhost = packet.query().findChild("streamhost", Namespace.BYTE_STREAMS);
506 final String host = streamhost == null ? null : streamhost.getAttribute("host");
507 final String port = streamhost == null ? null : streamhost.getAttribute("port");
508 if (host != null && port != null) {
509 try {
510 JingleCandidate candidate = new JingleCandidate(nextRandomId(), true);
511 candidate.setHost(host);
512 candidate.setPort(Integer.parseInt(port));
513 candidate.setType(JingleCandidate.TYPE_PROXY);
514 candidate.setJid(proxy);
515 candidate.setPriority(655360 + (initiator ? 30 : 0));
516 primaryCandidates.put(account.getJid().asBareJid(), candidate);
517 listener.onPrimaryCandidateFound(true, candidate);
518 } catch (final NumberFormatException e) {
519 listener.onPrimaryCandidateFound(false, null);
520 }
521 } else {
522 listener.onPrimaryCandidateFound(false, null);
523 }
524 }
525 });
526 } else {
527 listener.onPrimaryCandidateFound(false, null);
528 }
529
530 } else {
531 listener.onPrimaryCandidateFound(true,
532 this.primaryCandidates.get(account.getJid().asBareJid()));
533 }
534 }
535
536 public void retractSessionProposal(final Account account, final Jid with) {
537 synchronized (this.rtpSessionProposals) {
538 RtpSessionProposal matchingProposal = null;
539 for (RtpSessionProposal proposal : this.rtpSessionProposals.keySet()) {
540 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
541 matchingProposal = proposal;
542 break;
543 }
544 }
545 if (matchingProposal != null) {
546 retractSessionProposal(matchingProposal);
547 }
548 }
549 }
550
551 private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) {
552 final Account account = rtpSessionProposal.account;
553 toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media);
554 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + rtpSessionProposal.with);
555 this.rtpSessionProposals.remove(rtpSessionProposal);
556 final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
557 writeLogMissedOutgoing(account, rtpSessionProposal.with, rtpSessionProposal.sessionId, null, System.currentTimeMillis());
558 mXmppConnectionService.sendMessagePacket(account, messagePacket);
559 }
560
561 public String initializeRtpSession(final Account account, final Jid with, final Set<Media> media) {
562 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
563 final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid());
564 rtpConnection.setProposedMedia(media);
565 this.connections.put(id, rtpConnection);
566 rtpConnection.sendSessionInitiate();
567 return id.sessionId;
568 }
569
570 public void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
571 synchronized (this.rtpSessionProposals) {
572 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
573 RtpSessionProposal proposal = entry.getKey();
574 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
575 final DeviceDiscoveryState preexistingState = entry.getValue();
576 if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) {
577 final RtpEndUserState endUserState = preexistingState.toEndUserState();
578 toneManager.transition(endUserState, media);
579 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
580 account,
581 with,
582 proposal.sessionId,
583 endUserState
584 );
585 return;
586 }
587 }
588 }
589 if (isBusy()) {
590 if (hasMatchingRtpSession(account, with, media)) {
591 Log.d(Config.LOGTAG, "ignoring request to propose jingle session because the other party already created one for us");
592 return;
593 }
594 throw new IllegalStateException("There is already a running RTP session. This should have been caught by the UI");
595 }
596 final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media);
597 this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
598 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
599 account,
600 proposal.with,
601 proposal.sessionId,
602 RtpEndUserState.FINDING_DEVICE
603 );
604 final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
605 Log.d(Config.LOGTAG, messagePacket.toString());
606 mXmppConnectionService.sendMessagePacket(account, messagePacket);
607 }
608 }
609
610 public boolean hasMatchingProposal(final Account account, final Jid with) {
611 synchronized (this.rtpSessionProposals) {
612 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
613 final RtpSessionProposal proposal = entry.getKey();
614 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
615 return true;
616 }
617 }
618 }
619 return false;
620 }
621
622 public void deliverIbbPacket(Account account, IqPacket packet) {
623 final String sid;
624 final Element payload;
625 if (packet.hasChild("open", Namespace.IBB)) {
626 payload = packet.findChild("open", Namespace.IBB);
627 sid = payload.getAttribute("sid");
628 } else if (packet.hasChild("data", Namespace.IBB)) {
629 payload = packet.findChild("data", Namespace.IBB);
630 sid = payload.getAttribute("sid");
631 } else if (packet.hasChild("close", Namespace.IBB)) {
632 payload = packet.findChild("close", Namespace.IBB);
633 sid = payload.getAttribute("sid");
634 } else {
635 payload = null;
636 sid = null;
637 }
638 if (sid != null) {
639 for (final AbstractJingleConnection connection : this.connections.values()) {
640 if (connection instanceof JingleFileTransferConnection) {
641 final JingleFileTransferConnection fileTransfer = (JingleFileTransferConnection) connection;
642 final JingleTransport transport = fileTransfer.getTransport();
643 if (transport instanceof JingleInBandTransport) {
644 final JingleInBandTransport inBandTransport = (JingleInBandTransport) transport;
645 if (inBandTransport.matches(account, sid)) {
646 inBandTransport.deliverPayload(packet, payload);
647 }
648 return;
649 }
650 }
651 }
652 }
653 Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString());
654 account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
655 }
656
657 public void notifyRebound() {
658 for (final AbstractJingleConnection connection : this.connections.values()) {
659 connection.notifyRebound();
660 }
661 }
662
663 public WeakReference<JingleRtpConnection> findJingleRtpConnection(Account account, Jid with, String sessionId) {
664 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId);
665 final AbstractJingleConnection connection = connections.get(id);
666 if (connection instanceof JingleRtpConnection) {
667 return new WeakReference<>((JingleRtpConnection) connection);
668 }
669 return null;
670 }
671
672 public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) {
673 synchronized (this.rtpSessionProposals) {
674 final RtpSessionProposal sessionProposal = getRtpSessionProposal(account, from.asBareJid(), sessionId);
675 final DeviceDiscoveryState currentState = sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal);
676 if (currentState == null) {
677 Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId);
678 return;
679 }
680 if (currentState == DeviceDiscoveryState.DISCOVERED) {
681 Log.d(Config.LOGTAG, "session proposal already at discovered. not going to fall back");
682 return;
683 }
684 this.rtpSessionProposals.put(sessionProposal, target);
685 final RtpEndUserState endUserState = target.toEndUserState();
686 toneManager.transition(endUserState, sessionProposal.media);
687 mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, endUserState);
688 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target);
689 }
690 }
691
692 public void rejectRtpSession(final String sessionId) {
693 for (final AbstractJingleConnection connection : this.connections.values()) {
694 if (connection.getId().sessionId.equals(sessionId)) {
695 if (connection instanceof JingleRtpConnection) {
696 ((JingleRtpConnection) connection).rejectCall();
697 }
698 }
699 }
700 }
701
702 public void endRtpSession(final String sessionId) {
703 for (final AbstractJingleConnection connection : this.connections.values()) {
704 if (connection.getId().sessionId.equals(sessionId)) {
705 if (connection instanceof JingleRtpConnection) {
706 ((JingleRtpConnection) connection).endCall();
707 }
708 }
709 }
710 }
711
712 public void failProceed(Account account, final Jid with, String sessionId) {
713 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId);
714 final AbstractJingleConnection existingJingleConnection = connections.get(id);
715 if (existingJingleConnection instanceof JingleRtpConnection) {
716 ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed();
717 }
718 }
719
720 void ensureConnectionIsRegistered(final AbstractJingleConnection connection) {
721 if (connections.containsValue(connection)) {
722 return;
723 }
724 final IllegalStateException e = new IllegalStateException("JingleConnection has not been registered with connection manager");
725 Log.e(Config.LOGTAG, "ensureConnectionIsRegistered() failed. Going to throw", e);
726 throw e;
727 }
728
729 void setTerminalSessionState(AbstractJingleConnection.Id id, final RtpEndUserState state, final Set<Media> media) {
730 this.terminatedSessions.put(PersistableSessionId.of(id), new TerminatedRtpSession(state, media));
731 }
732
733 public TerminatedRtpSession getTerminalSessionState(final Jid with, final String sessionId) {
734 return this.terminatedSessions.getIfPresent(new PersistableSessionId(with, sessionId));
735 }
736
737 private static class PersistableSessionId {
738 private final Jid with;
739 private final String sessionId;
740
741 private PersistableSessionId(Jid with, String sessionId) {
742 this.with = with;
743 this.sessionId = sessionId;
744 }
745
746 public static PersistableSessionId of(AbstractJingleConnection.Id id) {
747 return new PersistableSessionId(id.with, id.sessionId);
748 }
749
750 @Override
751 public boolean equals(Object o) {
752 if (this == o) return true;
753 if (o == null || getClass() != o.getClass()) return false;
754 PersistableSessionId that = (PersistableSessionId) o;
755 return Objects.equal(with, that.with) &&
756 Objects.equal(sessionId, that.sessionId);
757 }
758
759 @Override
760 public int hashCode() {
761 return Objects.hashCode(with, sessionId);
762 }
763 }
764
765 public static class TerminatedRtpSession {
766 public final RtpEndUserState state;
767 public final Set<Media> media;
768
769 TerminatedRtpSession(RtpEndUserState state, Set<Media> media) {
770 this.state = state;
771 this.media = media;
772 }
773 }
774
775 public enum DeviceDiscoveryState {
776 SEARCHING, DISCOVERED, FAILED;
777
778 public RtpEndUserState toEndUserState() {
779 switch (this) {
780 case SEARCHING:
781 return RtpEndUserState.FINDING_DEVICE;
782 case DISCOVERED:
783 return RtpEndUserState.RINGING;
784 default:
785 return RtpEndUserState.CONNECTIVITY_ERROR;
786 }
787 }
788 }
789
790 public static class RtpSessionProposal implements OngoingRtpSession {
791 public final Jid with;
792 public final String sessionId;
793 public final Set<Media> media;
794 private final Account account;
795
796 private RtpSessionProposal(Account account, Jid with, String sessionId) {
797 this(account, with, sessionId, Collections.emptySet());
798 }
799
800 private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media) {
801 this.account = account;
802 this.with = with;
803 this.sessionId = sessionId;
804 this.media = media;
805 }
806
807 public static RtpSessionProposal of(Account account, Jid with, Set<Media> media) {
808 return new RtpSessionProposal(account, with, nextRandomId(), media);
809 }
810
811 @Override
812 public boolean equals(Object o) {
813 if (this == o) return true;
814 if (o == null || getClass() != o.getClass()) return false;
815 RtpSessionProposal proposal = (RtpSessionProposal) o;
816 return Objects.equal(account.getJid(), proposal.account.getJid()) &&
817 Objects.equal(with, proposal.with) &&
818 Objects.equal(sessionId, proposal.sessionId);
819 }
820
821 @Override
822 public int hashCode() {
823 return Objects.hashCode(account.getJid(), with, sessionId);
824 }
825
826 @Override
827 public Account getAccount() {
828 return account;
829 }
830
831 @Override
832 public Jid getWith() {
833 return with;
834 }
835
836 @Override
837 public String getSessionId() {
838 return sessionId;
839 }
840 }
841}