1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Log;
4import android.util.Pair;
5
6import java.io.FileNotFoundException;
7import java.io.InputStream;
8import java.io.OutputStream;
9import java.util.ArrayList;
10import java.util.Iterator;
11import java.util.List;
12import java.util.Locale;
13import java.util.Map.Entry;
14import java.util.concurrent.ConcurrentHashMap;
15
16import eu.siacs.conversations.Config;
17import eu.siacs.conversations.crypto.axolotl.AxolotlService;
18import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback;
19import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
20import eu.siacs.conversations.entities.Account;
21import eu.siacs.conversations.entities.Conversation;
22import eu.siacs.conversations.entities.DownloadableFile;
23import eu.siacs.conversations.entities.Message;
24import eu.siacs.conversations.entities.Presence;
25import eu.siacs.conversations.entities.ServiceDiscoveryResult;
26import eu.siacs.conversations.entities.Transferable;
27import eu.siacs.conversations.entities.TransferablePlaceholder;
28import eu.siacs.conversations.persistance.FileBackend;
29import eu.siacs.conversations.services.AbstractConnectionManager;
30import eu.siacs.conversations.services.XmppConnectionService;
31import eu.siacs.conversations.xml.Element;
32import eu.siacs.conversations.xmpp.OnIqPacketReceived;
33import eu.siacs.conversations.xmpp.jid.Jid;
34import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
35import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
36import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
37import eu.siacs.conversations.xmpp.stanzas.IqPacket;
38
39public class JingleConnection implements Transferable {
40
41 private JingleConnectionManager mJingleConnectionManager;
42 private XmppConnectionService mXmppConnectionService;
43
44 protected static final int JINGLE_STATUS_INITIATED = 0;
45 protected static final int JINGLE_STATUS_ACCEPTED = 1;
46 protected static final int JINGLE_STATUS_FINISHED = 4;
47 protected static final int JINGLE_STATUS_TRANSMITTING = 5;
48 protected static final int JINGLE_STATUS_FAILED = 99;
49
50 private Content.Version ftVersion = Content.Version.FT_3;
51
52 private int ibbBlockSize = 8192;
53
54 private int mJingleStatus = -1;
55 private int mStatus = Transferable.STATUS_UNKNOWN;
56 private Message message;
57 private String sessionId;
58 private Account account;
59 private Jid initiator;
60 private Jid responder;
61 private List<JingleCandidate> candidates = new ArrayList<>();
62 private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
63
64 private String transportId;
65 private Element fileOffer;
66 private DownloadableFile file = null;
67
68 private String contentName;
69 private String contentCreator;
70
71 private int mProgress = 0;
72
73 private boolean receivedCandidate = false;
74 private boolean sentCandidate = false;
75
76 private boolean acceptedAutomatically = false;
77
78 private XmppAxolotlMessage mXmppAxolotlMessage;
79
80 private JingleTransport transport = null;
81
82 private OutputStream mFileOutputStream;
83 private InputStream mFileInputStream;
84
85 private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
86
87 @Override
88 public void onIqPacketReceived(Account account, IqPacket packet) {
89 if (packet.getType() != IqPacket.TYPE.RESULT) {
90 fail();
91 }
92 }
93 };
94
95 final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() {
96
97 @Override
98 public void onFileTransmitted(DownloadableFile file) {
99 if (responder.equals(account.getJid())) {
100 sendSuccess();
101 mXmppConnectionService.getFileBackend().updateFileParams(message);
102 mXmppConnectionService.databaseBackend.createMessage(message);
103 mXmppConnectionService.markMessage(message,Message.STATUS_RECEIVED);
104 if (acceptedAutomatically) {
105 message.markUnread();
106 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
107 account.getPgpDecryptionService().decrypt(message, true);
108 } else {
109 JingleConnection.this.mXmppConnectionService.getNotificationService().push(message);
110 }
111 }
112 } else {
113 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
114 account.getPgpDecryptionService().decrypt(message, false);
115 }
116 if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
117 file.delete();
118 }
119 }
120 Log.d(Config.LOGTAG,"successfully transmitted file:" + file.getAbsolutePath()+" ("+file.getSha1Sum()+")");
121 if (message.getEncryption() != Message.ENCRYPTION_PGP) {
122 mXmppConnectionService.getFileBackend().updateMediaScanner(file);
123 }
124 }
125
126 @Override
127 public void onFileTransferAborted() {
128 JingleConnection.this.sendCancel();
129 JingleConnection.this.fail();
130 }
131 };
132
133 public InputStream getFileInputStream() {
134 return this.mFileInputStream;
135 }
136
137 public OutputStream getFileOutputStream() {
138 return this.mFileOutputStream;
139 }
140
141 private OnProxyActivated onProxyActivated = new OnProxyActivated() {
142
143 @Override
144 public void success() {
145 if (initiator.equals(account.getJid())) {
146 Log.d(Config.LOGTAG, "we were initiating. sending file");
147 transport.send(file, onFileTransmissionSatusChanged);
148 } else {
149 transport.receive(file, onFileTransmissionSatusChanged);
150 Log.d(Config.LOGTAG, "we were responding. receiving file");
151 }
152 }
153
154 @Override
155 public void failed() {
156 Log.d(Config.LOGTAG, "proxy activation failed");
157 }
158 };
159
160 public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
161 this.mJingleConnectionManager = mJingleConnectionManager;
162 this.mXmppConnectionService = mJingleConnectionManager
163 .getXmppConnectionService();
164 }
165
166 public String getSessionId() {
167 return this.sessionId;
168 }
169
170 public Account getAccount() {
171 return this.account;
172 }
173
174 public Jid getCounterPart() {
175 return this.message.getCounterpart();
176 }
177
178 public void deliverPacket(JinglePacket packet) {
179 boolean returnResult = true;
180 if (packet.isAction("session-terminate")) {
181 Reason reason = packet.getReason();
182 if (reason != null) {
183 if (reason.hasChild("cancel")) {
184 this.fail();
185 } else if (reason.hasChild("success")) {
186 this.receiveSuccess();
187 } else {
188 this.fail();
189 }
190 } else {
191 this.fail();
192 }
193 } else if (packet.isAction("session-accept")) {
194 returnResult = receiveAccept(packet);
195 } else if (packet.isAction("transport-info")) {
196 returnResult = receiveTransportInfo(packet);
197 } else if (packet.isAction("transport-replace")) {
198 if (packet.getJingleContent().hasIbbTransport()) {
199 returnResult = this.receiveFallbackToIbb(packet);
200 } else {
201 returnResult = false;
202 Log.d(Config.LOGTAG, "trying to fallback to something unknown"
203 + packet.toString());
204 }
205 } else if (packet.isAction("transport-accept")) {
206 returnResult = this.receiveTransportAccept(packet);
207 } else {
208 Log.d(Config.LOGTAG, "packet arrived in connection. action was "
209 + packet.getAction());
210 returnResult = false;
211 }
212 IqPacket response;
213 if (returnResult) {
214 response = packet.generateResponse(IqPacket.TYPE.RESULT);
215
216 } else {
217 response = packet.generateResponse(IqPacket.TYPE.ERROR);
218 }
219 mXmppConnectionService.sendIqPacket(account,response,null);
220 }
221
222 public void init(final Message message) {
223 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
224 Conversation conversation = message.getConversation();
225 conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, new OnMessageCreatedCallback() {
226 @Override
227 public void run(XmppAxolotlMessage xmppAxolotlMessage) {
228 if (xmppAxolotlMessage != null) {
229 init(message, xmppAxolotlMessage);
230 } else {
231 fail();
232 }
233 }
234 });
235 } else {
236 init(message, null);
237 }
238 }
239
240 private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
241 this.mXmppAxolotlMessage = xmppAxolotlMessage;
242 this.contentCreator = "initiator";
243 this.contentName = this.mJingleConnectionManager.nextRandomId();
244 this.message = message;
245 this.account = message.getConversation().getAccount();
246 upgradeNamespace();
247 this.message.setTransferable(this);
248 this.mStatus = Transferable.STATUS_UPLOADING;
249 this.initiator = this.account.getJid();
250 this.responder = this.message.getCounterpart();
251 this.sessionId = this.mJingleConnectionManager.nextRandomId();
252 this.transportId = this.mJingleConnectionManager.nextRandomId();
253 if (this.candidates.size() > 0) {
254 this.sendInitRequest();
255 } else {
256 this.mJingleConnectionManager.getPrimaryCandidate(account,
257 new OnPrimaryCandidateFound() {
258
259 @Override
260 public void onPrimaryCandidateFound(boolean success,
261 final JingleCandidate candidate) {
262 if (success) {
263 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
264 JingleConnection.this, candidate);
265 connections.put(candidate.getCid(),
266 socksConnection);
267 socksConnection
268 .connect(new OnTransportConnected() {
269
270 @Override
271 public void failed() {
272 Log.d(Config.LOGTAG,
273 "connection to our own primary candidete failed");
274 sendInitRequest();
275 }
276
277 @Override
278 public void established() {
279 Log.d(Config.LOGTAG,
280 "successfully connected to our own primary candidate");
281 mergeCandidate(candidate);
282 sendInitRequest();
283 }
284 });
285 mergeCandidate(candidate);
286 } else {
287 Log.d(Config.LOGTAG, "no primary candidate of our own was found");
288 sendInitRequest();
289 }
290 }
291 });
292 }
293
294 }
295
296 private void upgradeNamespace() {
297 Jid jid = this.message.getCounterpart();
298 String resource = jid != null ?jid.getResourcepart() : null;
299 if (resource != null) {
300 Presence presence = this.account.getRoster().getContact(jid).getPresences().getPresences().get(resource);
301 ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
302 if (result != null) {
303 List<String> features = result.getFeatures();
304 if (features.contains(Content.Version.FT_4.getNamespace())) {
305 this.ftVersion = Content.Version.FT_4;
306 }
307 }
308 }
309 }
310
311 public void init(Account account, JinglePacket packet) {
312 this.mJingleStatus = JINGLE_STATUS_INITIATED;
313 Conversation conversation = this.mXmppConnectionService
314 .findOrCreateConversation(account,
315 packet.getFrom().toBareJid(), false);
316 this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
317 this.message.setStatus(Message.STATUS_RECEIVED);
318 this.mStatus = Transferable.STATUS_OFFER;
319 this.message.setTransferable(this);
320 final Jid from = packet.getFrom();
321 this.message.setCounterpart(from);
322 this.account = account;
323 this.initiator = packet.getFrom();
324 this.responder = this.account.getJid();
325 this.sessionId = packet.getSessionId();
326 Content content = packet.getJingleContent();
327 this.contentCreator = content.getAttribute("creator");
328 this.contentName = content.getAttribute("name");
329 this.transportId = content.getTransportId();
330 this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
331 this.ftVersion = content.getVersion();
332 if (ftVersion == null) {
333 this.sendCancel();
334 this.fail();
335 return;
336 }
337 this.fileOffer = content.getFileOffer(this.ftVersion);
338
339 mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null);
340
341 if (fileOffer != null) {
342 Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
343 if (encrypted != null) {
344 this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().toBareJid());
345 }
346 Element fileSize = fileOffer.findChild("size");
347 Element fileNameElement = fileOffer.findChild("name");
348 if (fileNameElement != null) {
349 String[] filename = fileNameElement.getContent()
350 .toLowerCase(Locale.US).toLowerCase().split("\\.");
351 String extension = filename[filename.length - 1];
352 if (VALID_IMAGE_EXTENSIONS.contains(extension)) {
353 message.setType(Message.TYPE_IMAGE);
354 message.setRelativeFilePath(message.getUuid()+"."+extension);
355 } else if (VALID_CRYPTO_EXTENSIONS.contains(
356 filename[filename.length - 1])) {
357 if (filename.length == 3) {
358 extension = filename[filename.length - 2];
359 if (VALID_IMAGE_EXTENSIONS.contains(extension)) {
360 message.setType(Message.TYPE_IMAGE);
361 message.setRelativeFilePath(message.getUuid()+"."+extension);
362 } else {
363 message.setType(Message.TYPE_FILE);
364 }
365 if (filename[filename.length - 1].equals("otr")) {
366 message.setEncryption(Message.ENCRYPTION_OTR);
367 } else {
368 message.setEncryption(Message.ENCRYPTION_PGP);
369 }
370 }
371 } else {
372 message.setType(Message.TYPE_FILE);
373 }
374 if (message.getType() == Message.TYPE_FILE) {
375 String suffix = "";
376 if (!fileNameElement.getContent().isEmpty()) {
377 String parts[] = fileNameElement.getContent().split("/");
378 suffix = parts[parts.length - 1];
379 if (message.getEncryption() == Message.ENCRYPTION_OTR && suffix.endsWith(".otr")) {
380 suffix = suffix.substring(0,suffix.length() - 4);
381 } else if (message.getEncryption() == Message.ENCRYPTION_PGP && (suffix.endsWith(".pgp") || suffix.endsWith(".gpg"))) {
382 suffix = suffix.substring(0,suffix.length() - 4);
383 }
384 }
385 message.setRelativeFilePath(message.getUuid()+"_"+suffix);
386 }
387 long size = Long.parseLong(fileSize.getContent());
388 message.setBody(Long.toString(size));
389 conversation.add(message);
390 mXmppConnectionService.updateConversationUi();
391 if (mJingleConnectionManager.hasStoragePermission()
392 && size < this.mJingleConnectionManager.getAutoAcceptFileSize()
393 && mXmppConnectionService.isDataSaverDisabled()) {
394 Log.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom());
395 this.acceptedAutomatically = true;
396 this.sendAccept();
397 } else {
398 message.markUnread();
399 Log.d(Config.LOGTAG,
400 "not auto accepting new file offer with size: "
401 + size
402 + " allowed size:"
403 + this.mJingleConnectionManager
404 .getAutoAcceptFileSize());
405 this.mXmppConnectionService.getNotificationService().push(message);
406 }
407 this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
408 if (mXmppAxolotlMessage != null) {
409 XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage);
410 if (transportMessage != null) {
411 message.setEncryption(Message.ENCRYPTION_AXOLOTL);
412 this.file.setKey(transportMessage.getKey());
413 this.file.setIv(transportMessage.getIv());
414 message.setFingerprint(transportMessage.getFingerprint());
415 } else {
416 Log.d(Config.LOGTAG,"could not process KeyTransportMessage");
417 }
418 } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
419 byte[] key = conversation.getSymmetricKey();
420 if (key == null) {
421 this.sendCancel();
422 this.fail();
423 return;
424 } else {
425 this.file.setKeyAndIv(key);
426 }
427 }
428 this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file,message.getEncryption() == Message.ENCRYPTION_AXOLOTL);
429 if (message.getEncryption() == Message.ENCRYPTION_OTR && Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE) {
430 this.file.setExpectedSize((size / 16 + 1) * 16);
431 } else {
432 this.file.setExpectedSize(size);
433 }
434 Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
435 } else {
436 this.sendCancel();
437 this.fail();
438 }
439 } else {
440 this.sendCancel();
441 this.fail();
442 }
443 }
444
445 private void sendInitRequest() {
446 JinglePacket packet = this.bootstrapPacket("session-initiate");
447 Content content = new Content(this.contentCreator, this.contentName);
448 if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
449 content.setTransportId(this.transportId);
450 this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
451 Pair<InputStream,Integer> pair;
452 try {
453 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
454 Conversation conversation = this.message.getConversation();
455 if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) {
456 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not set symmetric key");
457 cancel();
458 }
459 this.file.setKeyAndIv(conversation.getSymmetricKey());
460 pair = AbstractConnectionManager.createInputStream(this.file, false);
461 this.file.setExpectedSize(pair.second);
462 content.setFileOffer(this.file, true, this.ftVersion);
463 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
464 this.file.setKey(mXmppAxolotlMessage.getInnerKey());
465 this.file.setIv(mXmppAxolotlMessage.getIV());
466 pair = AbstractConnectionManager.createInputStream(this.file, true);
467 this.file.setExpectedSize(pair.second);
468 content.setFileOffer(this.file, false, this.ftVersion).addChild(mXmppAxolotlMessage.toElement());
469 } else {
470 pair = AbstractConnectionManager.createInputStream(this.file, false);
471 this.file.setExpectedSize(pair.second);
472 content.setFileOffer(this.file, false, this.ftVersion);
473 }
474 } catch (FileNotFoundException e) {
475 cancel();
476 return;
477 }
478 this.mFileInputStream = pair.first;
479 content.setTransportId(this.transportId);
480 content.socks5transport().setChildren(getCandidatesAsElements());
481 packet.setContent(content);
482 this.sendJinglePacket(packet,new OnIqPacketReceived() {
483
484 @Override
485 public void onIqPacketReceived(Account account, IqPacket packet) {
486 if (packet.getType() == IqPacket.TYPE.RESULT) {
487 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer");
488 mJingleStatus = JINGLE_STATUS_INITIATED;
489 mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
490 } else {
491 fail();
492 }
493 }
494 });
495
496 }
497 }
498
499 private List<Element> getCandidatesAsElements() {
500 List<Element> elements = new ArrayList<>();
501 for (JingleCandidate c : this.candidates) {
502 if (c.isOurs()) {
503 elements.add(c.toElement());
504 }
505 }
506 return elements;
507 }
508
509 private void sendAccept() {
510 mJingleStatus = JINGLE_STATUS_ACCEPTED;
511 this.mStatus = Transferable.STATUS_DOWNLOADING;
512 mXmppConnectionService.updateConversationUi();
513 this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
514 @Override
515 public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
516 final JinglePacket packet = bootstrapPacket("session-accept");
517 final Content content = new Content(contentCreator,contentName);
518 content.setFileOffer(fileOffer, ftVersion);
519 content.setTransportId(transportId);
520 if (success && candidate != null && !equalCandidateExists(candidate)) {
521 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
522 JingleConnection.this,
523 candidate);
524 connections.put(candidate.getCid(), socksConnection);
525 socksConnection.connect(new OnTransportConnected() {
526
527 @Override
528 public void failed() {
529 Log.d(Config.LOGTAG,"connection to our own primary candidate failed");
530 content.socks5transport().setChildren(getCandidatesAsElements());
531 packet.setContent(content);
532 sendJinglePacket(packet);
533 connectNextCandidate();
534 }
535
536 @Override
537 public void established() {
538 Log.d(Config.LOGTAG, "connected to primary candidate");
539 mergeCandidate(candidate);
540 content.socks5transport().setChildren(getCandidatesAsElements());
541 packet.setContent(content);
542 sendJinglePacket(packet);
543 connectNextCandidate();
544 }
545 });
546 } else {
547 Log.d(Config.LOGTAG,"did not find a primary candidate for ourself");
548 content.socks5transport().setChildren(getCandidatesAsElements());
549 packet.setContent(content);
550 sendJinglePacket(packet);
551 connectNextCandidate();
552 }
553 }
554 });
555 }
556
557 private JinglePacket bootstrapPacket(String action) {
558 JinglePacket packet = new JinglePacket();
559 packet.setAction(action);
560 packet.setFrom(account.getJid());
561 packet.setTo(this.message.getCounterpart());
562 packet.setSessionId(this.sessionId);
563 packet.setInitiator(this.initiator);
564 return packet;
565 }
566
567 private void sendJinglePacket(JinglePacket packet) {
568 mXmppConnectionService.sendIqPacket(account,packet,responseListener);
569 }
570
571 private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
572 mXmppConnectionService.sendIqPacket(account,packet,callback);
573 }
574
575 private boolean receiveAccept(JinglePacket packet) {
576 Content content = packet.getJingleContent();
577 mergeCandidates(JingleCandidate.parse(content.socks5transport()
578 .getChildren()));
579 this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
580 mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
581 this.connectNextCandidate();
582 return true;
583 }
584
585 private boolean receiveTransportInfo(JinglePacket packet) {
586 Content content = packet.getJingleContent();
587 if (content.hasSocks5Transport()) {
588 if (content.socks5transport().hasChild("activated")) {
589 if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
590 onProxyActivated.success();
591 } else {
592 String cid = content.socks5transport().findChild("activated").getAttribute("cid");
593 Log.d(Config.LOGTAG, "received proxy activated (" + cid
594 + ")prior to choosing our own transport");
595 JingleSocks5Transport connection = this.connections.get(cid);
596 if (connection != null) {
597 connection.setActivated(true);
598 } else {
599 Log.d(Config.LOGTAG, "activated connection not found");
600 this.sendCancel();
601 this.fail();
602 }
603 }
604 return true;
605 } else if (content.socks5transport().hasChild("proxy-error")) {
606 onProxyActivated.failed();
607 return true;
608 } else if (content.socks5transport().hasChild("candidate-error")) {
609 Log.d(Config.LOGTAG, "received candidate error");
610 this.receivedCandidate = true;
611 if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
612 && (this.sentCandidate)) {
613 this.connect();
614 }
615 return true;
616 } else if (content.socks5transport().hasChild("candidate-used")) {
617 String cid = content.socks5transport()
618 .findChild("candidate-used").getAttribute("cid");
619 if (cid != null) {
620 Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
621 JingleCandidate candidate = getCandidate(cid);
622 candidate.flagAsUsedByCounterpart();
623 this.receivedCandidate = true;
624 if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
625 && (this.sentCandidate)) {
626 this.connect();
627 } else {
628 Log.d(Config.LOGTAG,
629 "ignoring because file is already in transmission or we haven't sent our candidate yet");
630 }
631 return true;
632 } else {
633 return false;
634 }
635 } else {
636 return false;
637 }
638 } else {
639 return true;
640 }
641 }
642
643 private void connect() {
644 final JingleSocks5Transport connection = chooseConnection();
645 this.transport = connection;
646 if (connection == null) {
647 Log.d(Config.LOGTAG, "could not find suitable candidate");
648 this.disconnectSocks5Connections();
649 if (this.initiator.equals(account.getJid())) {
650 this.sendFallbackToIbb();
651 }
652 } else {
653 this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
654 if (connection.needsActivation()) {
655 if (connection.getCandidate().isOurs()) {
656 final String sid;
657 if (ftVersion == Content.Version.FT_3) {
658 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": use session ID instead of transport ID to activate proxy");
659 sid = getSessionId();
660 } else {
661 sid = getTransportId();
662 }
663 Log.d(Config.LOGTAG, "candidate "
664 + connection.getCandidate().getCid()
665 + " was our proxy. going to activate");
666 IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
667 activation.setTo(connection.getCandidate().getJid());
668 activation.query("http://jabber.org/protocol/bytestreams")
669 .setAttribute("sid", sid);
670 activation.query().addChild("activate")
671 .setContent(this.getCounterPart().toString());
672 mXmppConnectionService.sendIqPacket(account,activation,
673 new OnIqPacketReceived() {
674
675 @Override
676 public void onIqPacketReceived(Account account,
677 IqPacket packet) {
678 if (packet.getType() != IqPacket.TYPE.RESULT) {
679 onProxyActivated.failed();
680 } else {
681 onProxyActivated.success();
682 sendProxyActivated(connection.getCandidate().getCid());
683 }
684 }
685 });
686 } else {
687 Log.d(Config.LOGTAG,
688 "candidate "
689 + connection.getCandidate().getCid()
690 + " was a proxy. waiting for other party to activate");
691 }
692 } else {
693 if (initiator.equals(account.getJid())) {
694 Log.d(Config.LOGTAG, "we were initiating. sending file");
695 connection.send(file, onFileTransmissionSatusChanged);
696 } else {
697 Log.d(Config.LOGTAG, "we were responding. receiving file");
698 connection.receive(file, onFileTransmissionSatusChanged);
699 }
700 }
701 }
702 }
703
704 private JingleSocks5Transport chooseConnection() {
705 JingleSocks5Transport connection = null;
706 for (Entry<String, JingleSocks5Transport> cursor : connections
707 .entrySet()) {
708 JingleSocks5Transport currentConnection = cursor.getValue();
709 // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
710 if (currentConnection.isEstablished()
711 && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
712 .getCandidate().isOurs()))) {
713 // Log.d(Config.LOGTAG,"is usable");
714 if (connection == null) {
715 connection = currentConnection;
716 } else {
717 if (connection.getCandidate().getPriority() < currentConnection
718 .getCandidate().getPriority()) {
719 connection = currentConnection;
720 } else if (connection.getCandidate().getPriority() == currentConnection
721 .getCandidate().getPriority()) {
722 // Log.d(Config.LOGTAG,"found two candidates with same priority");
723 if (initiator.equals(account.getJid())) {
724 if (currentConnection.getCandidate().isOurs()) {
725 connection = currentConnection;
726 }
727 } else {
728 if (!currentConnection.getCandidate().isOurs()) {
729 connection = currentConnection;
730 }
731 }
732 }
733 }
734 }
735 }
736 return connection;
737 }
738
739 private void sendSuccess() {
740 JinglePacket packet = bootstrapPacket("session-terminate");
741 Reason reason = new Reason();
742 reason.addChild("success");
743 packet.setReason(reason);
744 this.sendJinglePacket(packet);
745 this.disconnectSocks5Connections();
746 this.mJingleStatus = JINGLE_STATUS_FINISHED;
747 this.message.setStatus(Message.STATUS_RECEIVED);
748 this.message.setTransferable(null);
749 this.mXmppConnectionService.updateMessage(message);
750 this.mJingleConnectionManager.finishConnection(this);
751 }
752
753 private void sendFallbackToIbb() {
754 Log.d(Config.LOGTAG, "sending fallback to ibb");
755 JinglePacket packet = this.bootstrapPacket("transport-replace");
756 Content content = new Content(this.contentCreator, this.contentName);
757 this.transportId = this.mJingleConnectionManager.nextRandomId();
758 content.setTransportId(this.transportId);
759 content.ibbTransport().setAttribute("block-size",
760 Integer.toString(this.ibbBlockSize));
761 packet.setContent(content);
762 this.sendJinglePacket(packet);
763 }
764
765 private boolean receiveFallbackToIbb(JinglePacket packet) {
766 Log.d(Config.LOGTAG, "receiving fallack to ibb");
767 String receivedBlockSize = packet.getJingleContent().ibbTransport()
768 .getAttribute("block-size");
769 if (receivedBlockSize != null) {
770 int bs = Integer.parseInt(receivedBlockSize);
771 if (bs > this.ibbBlockSize) {
772 this.ibbBlockSize = bs;
773 }
774 }
775 this.transportId = packet.getJingleContent().getTransportId();
776 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
777 this.transport.receive(file, onFileTransmissionSatusChanged);
778 JinglePacket answer = bootstrapPacket("transport-accept");
779 Content content = new Content("initiator", "a-file-offer");
780 content.setTransportId(this.transportId);
781 content.ibbTransport().setAttribute("block-size",this.ibbBlockSize);
782 answer.setContent(content);
783 this.sendJinglePacket(answer);
784 return true;
785 }
786
787 private boolean receiveTransportAccept(JinglePacket packet) {
788 if (packet.getJingleContent().hasIbbTransport()) {
789 String receivedBlockSize = packet.getJingleContent().ibbTransport()
790 .getAttribute("block-size");
791 if (receivedBlockSize != null) {
792 int bs = Integer.parseInt(receivedBlockSize);
793 if (bs > this.ibbBlockSize) {
794 this.ibbBlockSize = bs;
795 }
796 }
797 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
798 this.transport.connect(new OnTransportConnected() {
799
800 @Override
801 public void failed() {
802 Log.d(Config.LOGTAG, "ibb open failed");
803 }
804
805 @Override
806 public void established() {
807 JingleConnection.this.transport.send(file,
808 onFileTransmissionSatusChanged);
809 }
810 });
811 return true;
812 } else {
813 return false;
814 }
815 }
816
817 private void receiveSuccess() {
818 this.mJingleStatus = JINGLE_STATUS_FINISHED;
819 this.mXmppConnectionService.markMessage(this.message,Message.STATUS_SEND_RECEIVED);
820 this.disconnectSocks5Connections();
821 if (this.transport != null && this.transport instanceof JingleInbandTransport) {
822 this.transport.disconnect();
823 }
824 this.message.setTransferable(null);
825 this.mJingleConnectionManager.finishConnection(this);
826 }
827
828 public void cancel() {
829 this.disconnectSocks5Connections();
830 if (this.transport != null && this.transport instanceof JingleInbandTransport) {
831 this.transport.disconnect();
832 }
833 this.sendCancel();
834 this.mJingleConnectionManager.finishConnection(this);
835 if (this.responder.equals(account.getJid())) {
836 this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
837 if (this.file!=null) {
838 file.delete();
839 }
840 this.mXmppConnectionService.updateConversationUi();
841 } else {
842 this.mXmppConnectionService.markMessage(this.message,
843 Message.STATUS_SEND_FAILED);
844 this.message.setTransferable(null);
845 }
846 }
847
848 private void fail() {
849 this.mJingleStatus = JINGLE_STATUS_FAILED;
850 this.disconnectSocks5Connections();
851 if (this.transport != null && this.transport instanceof JingleInbandTransport) {
852 this.transport.disconnect();
853 }
854 FileBackend.close(mFileInputStream);
855 FileBackend.close(mFileOutputStream);
856 if (this.message != null) {
857 if (this.responder.equals(account.getJid())) {
858 this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
859 if (this.file!=null) {
860 file.delete();
861 }
862 this.mXmppConnectionService.updateConversationUi();
863 } else {
864 this.mXmppConnectionService.markMessage(this.message,
865 Message.STATUS_SEND_FAILED);
866 this.message.setTransferable(null);
867 }
868 }
869 this.mJingleConnectionManager.finishConnection(this);
870 }
871
872 private void sendCancel() {
873 JinglePacket packet = bootstrapPacket("session-terminate");
874 Reason reason = new Reason();
875 reason.addChild("cancel");
876 packet.setReason(reason);
877 this.sendJinglePacket(packet);
878 }
879
880 private void connectNextCandidate() {
881 for (JingleCandidate candidate : this.candidates) {
882 if ((!connections.containsKey(candidate.getCid()) && (!candidate
883 .isOurs()))) {
884 this.connectWithCandidate(candidate);
885 return;
886 }
887 }
888 this.sendCandidateError();
889 }
890
891 private void connectWithCandidate(final JingleCandidate candidate) {
892 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
893 this, candidate);
894 connections.put(candidate.getCid(), socksConnection);
895 socksConnection.connect(new OnTransportConnected() {
896
897 @Override
898 public void failed() {
899 Log.d(Config.LOGTAG,
900 "connection failed with " + candidate.getHost() + ":"
901 + candidate.getPort());
902 connectNextCandidate();
903 }
904
905 @Override
906 public void established() {
907 Log.d(Config.LOGTAG,
908 "established connection with " + candidate.getHost()
909 + ":" + candidate.getPort());
910 sendCandidateUsed(candidate.getCid());
911 }
912 });
913 }
914
915 private void disconnectSocks5Connections() {
916 Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
917 .entrySet().iterator();
918 while (it.hasNext()) {
919 Entry<String, JingleSocks5Transport> pairs = it.next();
920 pairs.getValue().disconnect();
921 it.remove();
922 }
923 }
924
925 private void sendProxyActivated(String cid) {
926 JinglePacket packet = bootstrapPacket("transport-info");
927 Content content = new Content(this.contentCreator, this.contentName);
928 content.setTransportId(this.transportId);
929 content.socks5transport().addChild("activated")
930 .setAttribute("cid", cid);
931 packet.setContent(content);
932 this.sendJinglePacket(packet);
933 }
934
935 private void sendCandidateUsed(final String cid) {
936 JinglePacket packet = bootstrapPacket("transport-info");
937 Content content = new Content(this.contentCreator, this.contentName);
938 content.setTransportId(this.transportId);
939 content.socks5transport().addChild("candidate-used")
940 .setAttribute("cid", cid);
941 packet.setContent(content);
942 this.sentCandidate = true;
943 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
944 connect();
945 }
946 this.sendJinglePacket(packet);
947 }
948
949 private void sendCandidateError() {
950 JinglePacket packet = bootstrapPacket("transport-info");
951 Content content = new Content(this.contentCreator, this.contentName);
952 content.setTransportId(this.transportId);
953 content.socks5transport().addChild("candidate-error");
954 packet.setContent(content);
955 this.sentCandidate = true;
956 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
957 connect();
958 }
959 this.sendJinglePacket(packet);
960 }
961
962 public Jid getInitiator() {
963 return this.initiator;
964 }
965
966 public Jid getResponder() {
967 return this.responder;
968 }
969
970 public int getJingleStatus() {
971 return this.mJingleStatus;
972 }
973
974 private boolean equalCandidateExists(JingleCandidate candidate) {
975 for (JingleCandidate c : this.candidates) {
976 if (c.equalValues(candidate)) {
977 return true;
978 }
979 }
980 return false;
981 }
982
983 private void mergeCandidate(JingleCandidate candidate) {
984 for (JingleCandidate c : this.candidates) {
985 if (c.equals(candidate)) {
986 return;
987 }
988 }
989 this.candidates.add(candidate);
990 }
991
992 private void mergeCandidates(List<JingleCandidate> candidates) {
993 for (JingleCandidate c : candidates) {
994 mergeCandidate(c);
995 }
996 }
997
998 private JingleCandidate getCandidate(String cid) {
999 for (JingleCandidate c : this.candidates) {
1000 if (c.getCid().equals(cid)) {
1001 return c;
1002 }
1003 }
1004 return null;
1005 }
1006
1007 public void updateProgress(int i) {
1008 this.mProgress = i;
1009 mXmppConnectionService.updateConversationUi();
1010 }
1011
1012 public String getTransportId() {
1013 return this.transportId;
1014 }
1015
1016 public Content.Version getFtVersion() {
1017 return this.ftVersion;
1018 }
1019
1020 interface OnProxyActivated {
1021 public void success();
1022
1023 public void failed();
1024 }
1025
1026 public boolean hasTransportId(String sid) {
1027 return sid.equals(this.transportId);
1028 }
1029
1030 public JingleTransport getTransport() {
1031 return this.transport;
1032 }
1033
1034 public boolean start() {
1035 if (account.getStatus() == Account.State.ONLINE) {
1036 if (mJingleStatus == JINGLE_STATUS_INITIATED) {
1037 new Thread(new Runnable() {
1038
1039 @Override
1040 public void run() {
1041 sendAccept();
1042 }
1043 }).start();
1044 }
1045 return true;
1046 } else {
1047 return false;
1048 }
1049 }
1050
1051 @Override
1052 public int getStatus() {
1053 return this.mStatus;
1054 }
1055
1056 @Override
1057 public long getFileSize() {
1058 if (this.file != null) {
1059 return this.file.getExpectedSize();
1060 } else {
1061 return 0;
1062 }
1063 }
1064
1065 @Override
1066 public int getProgress() {
1067 return this.mProgress;
1068 }
1069
1070 public AbstractConnectionManager getConnectionManager() {
1071 return this.mJingleConnectionManager;
1072 }
1073}