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