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