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