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