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.InputStream;
9import java.io.OutputStream;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Iterator;
13import java.util.List;
14import java.util.Locale;
15import java.util.Map.Entry;
16import java.util.concurrent.ConcurrentHashMap;
17
18import eu.siacs.conversations.Config;
19import eu.siacs.conversations.crypto.axolotl.AxolotlService;
20import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback;
21import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
22import eu.siacs.conversations.entities.Account;
23import eu.siacs.conversations.entities.Conversation;
24import eu.siacs.conversations.entities.DownloadableFile;
25import eu.siacs.conversations.entities.Message;
26import eu.siacs.conversations.entities.Transferable;
27import eu.siacs.conversations.entities.TransferablePlaceholder;
28import eu.siacs.conversations.persistance.FileBackend;
29import eu.siacs.conversations.services.AbstractConnectionManager;
30import eu.siacs.conversations.services.XmppConnectionService;
31import eu.siacs.conversations.utils.Xmlns;
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.ERROR) {
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 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
418 Conversation conversation = this.message.getConversation();
419 if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) {
420 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not set symmetric key");
421 cancel();
422 }
423 this.file.setKeyAndIv(conversation.getSymmetricKey());
424 pair = AbstractConnectionManager.createInputStream(this.file,false);
425 this.file.setExpectedSize(pair.second);
426 content.setFileOffer(this.file, true);
427 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
428 this.file.setKey(mXmppAxolotlMessage.getInnerKey());
429 this.file.setIv(mXmppAxolotlMessage.getIV());
430 pair = AbstractConnectionManager.createInputStream(this.file,true);
431 this.file.setExpectedSize(pair.second);
432 content.setFileOffer(this.file, false).addChild(mXmppAxolotlMessage.toElement());
433 } else {
434 pair = AbstractConnectionManager.createInputStream(this.file,false);
435 this.file.setExpectedSize(pair.second);
436 content.setFileOffer(this.file, false);
437 }
438 this.mFileInputStream = pair.first;
439 this.transportId = this.mJingleConnectionManager.nextRandomId();
440 content.setTransportId(this.transportId);
441 content.socks5transport().setChildren(getCandidatesAsElements());
442 packet.setContent(content);
443 this.sendJinglePacket(packet,new OnIqPacketReceived() {
444
445 @Override
446 public void onIqPacketReceived(Account account, IqPacket packet) {
447 if (packet.getType() != IqPacket.TYPE.ERROR) {
448 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer");
449 mJingleStatus = JINGLE_STATUS_INITIATED;
450 mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
451 } else {
452 fail();
453 }
454 }
455 });
456
457 }
458 }
459
460 private List<Element> getCandidatesAsElements() {
461 List<Element> elements = new ArrayList<>();
462 for (JingleCandidate c : this.candidates) {
463 if (c.isOurs()) {
464 elements.add(c.toElement());
465 }
466 }
467 return elements;
468 }
469
470 private void sendAccept() {
471 mJingleStatus = JINGLE_STATUS_ACCEPTED;
472 this.mStatus = Transferable.STATUS_DOWNLOADING;
473 mXmppConnectionService.updateConversationUi();
474 this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
475 @Override
476 public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
477 final JinglePacket packet = bootstrapPacket("session-accept");
478 final Content content = new Content(contentCreator,contentName);
479 content.setFileOffer(fileOffer);
480 content.setTransportId(transportId);
481 if (success && candidate != null && !equalCandidateExists(candidate)) {
482 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
483 JingleConnection.this,
484 candidate);
485 connections.put(candidate.getCid(), socksConnection);
486 socksConnection.connect(new OnTransportConnected() {
487
488 @Override
489 public void failed() {
490 Log.d(Config.LOGTAG,"connection to our own primary candidate failed");
491 content.socks5transport().setChildren(getCandidatesAsElements());
492 packet.setContent(content);
493 sendJinglePacket(packet);
494 connectNextCandidate();
495 }
496
497 @Override
498 public void established() {
499 Log.d(Config.LOGTAG, "connected to primary candidate");
500 mergeCandidate(candidate);
501 content.socks5transport().setChildren(getCandidatesAsElements());
502 packet.setContent(content);
503 sendJinglePacket(packet);
504 connectNextCandidate();
505 }
506 });
507 } else {
508 Log.d(Config.LOGTAG,"did not find a primary candidate for ourself");
509 content.socks5transport().setChildren(getCandidatesAsElements());
510 packet.setContent(content);
511 sendJinglePacket(packet);
512 connectNextCandidate();
513 }
514 }
515 });
516 }
517
518 private JinglePacket bootstrapPacket(String action) {
519 JinglePacket packet = new JinglePacket();
520 packet.setAction(action);
521 packet.setFrom(account.getJid());
522 packet.setTo(this.message.getCounterpart());
523 packet.setSessionId(this.sessionId);
524 packet.setInitiator(this.initiator);
525 return packet;
526 }
527
528 private void sendJinglePacket(JinglePacket packet) {
529 mXmppConnectionService.sendIqPacket(account,packet,responseListener);
530 }
531
532 private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
533 mXmppConnectionService.sendIqPacket(account,packet,callback);
534 }
535
536 private boolean receiveAccept(JinglePacket packet) {
537 Content content = packet.getJingleContent();
538 mergeCandidates(JingleCandidate.parse(content.socks5transport()
539 .getChildren()));
540 this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
541 mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
542 this.connectNextCandidate();
543 return true;
544 }
545
546 private boolean receiveTransportInfo(JinglePacket packet) {
547 Content content = packet.getJingleContent();
548 if (content.hasSocks5Transport()) {
549 if (content.socks5transport().hasChild("activated")) {
550 if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
551 onProxyActivated.success();
552 } else {
553 String cid = content.socks5transport().findChild("activated").getAttribute("cid");
554 Log.d(Config.LOGTAG, "received proxy activated (" + cid
555 + ")prior to choosing our own transport");
556 JingleSocks5Transport connection = this.connections.get(cid);
557 if (connection != null) {
558 connection.setActivated(true);
559 } else {
560 Log.d(Config.LOGTAG, "activated connection not found");
561 this.sendCancel();
562 this.fail();
563 }
564 }
565 return true;
566 } else if (content.socks5transport().hasChild("proxy-error")) {
567 onProxyActivated.failed();
568 return true;
569 } else if (content.socks5transport().hasChild("candidate-error")) {
570 Log.d(Config.LOGTAG, "received candidate error");
571 this.receivedCandidate = true;
572 if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
573 && (this.sentCandidate)) {
574 this.connect();
575 }
576 return true;
577 } else if (content.socks5transport().hasChild("candidate-used")) {
578 String cid = content.socks5transport()
579 .findChild("candidate-used").getAttribute("cid");
580 if (cid != null) {
581 Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
582 JingleCandidate candidate = getCandidate(cid);
583 candidate.flagAsUsedByCounterpart();
584 this.receivedCandidate = true;
585 if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
586 && (this.sentCandidate)) {
587 this.connect();
588 } else {
589 Log.d(Config.LOGTAG,
590 "ignoring because file is already in transmission or we havent sent our candidate yet");
591 }
592 return true;
593 } else {
594 return false;
595 }
596 } else {
597 return false;
598 }
599 } else {
600 return true;
601 }
602 }
603
604 private void connect() {
605 final JingleSocks5Transport connection = chooseConnection();
606 this.transport = connection;
607 if (connection == null) {
608 Log.d(Config.LOGTAG, "could not find suitable candidate");
609 this.disconnectSocks5Connections();
610 if (this.initiator.equals(account.getJid())) {
611 this.sendFallbackToIbb();
612 }
613 } else {
614 this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
615 if (connection.needsActivation()) {
616 if (connection.getCandidate().isOurs()) {
617 Log.d(Config.LOGTAG, "candidate "
618 + connection.getCandidate().getCid()
619 + " was our proxy. going to activate");
620 IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
621 activation.setTo(connection.getCandidate().getJid());
622 activation.query("http://jabber.org/protocol/bytestreams")
623 .setAttribute("sid", this.getSessionId());
624 activation.query().addChild("activate")
625 .setContent(this.getCounterPart().toString());
626 mXmppConnectionService.sendIqPacket(account,activation,
627 new OnIqPacketReceived() {
628
629 @Override
630 public void onIqPacketReceived(Account account,
631 IqPacket packet) {
632 if (packet.getType() == IqPacket.TYPE.ERROR) {
633 onProxyActivated.failed();
634 } else {
635 onProxyActivated.success();
636 sendProxyActivated(connection
637 .getCandidate().getCid());
638 }
639 }
640 });
641 } else {
642 Log.d(Config.LOGTAG,
643 "candidate "
644 + connection.getCandidate().getCid()
645 + " was a proxy. waiting for other party to activate");
646 }
647 } else {
648 if (initiator.equals(account.getJid())) {
649 Log.d(Config.LOGTAG, "we were initiating. sending file");
650 connection.send(file, onFileTransmissionSatusChanged);
651 } else {
652 Log.d(Config.LOGTAG, "we were responding. receiving file");
653 connection.receive(file, onFileTransmissionSatusChanged);
654 }
655 }
656 }
657 }
658
659 private JingleSocks5Transport chooseConnection() {
660 JingleSocks5Transport connection = null;
661 for (Entry<String, JingleSocks5Transport> cursor : connections
662 .entrySet()) {
663 JingleSocks5Transport currentConnection = cursor.getValue();
664 // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
665 if (currentConnection.isEstablished()
666 && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
667 .getCandidate().isOurs()))) {
668 // Log.d(Config.LOGTAG,"is usable");
669 if (connection == null) {
670 connection = currentConnection;
671 } else {
672 if (connection.getCandidate().getPriority() < currentConnection
673 .getCandidate().getPriority()) {
674 connection = currentConnection;
675 } else if (connection.getCandidate().getPriority() == currentConnection
676 .getCandidate().getPriority()) {
677 // Log.d(Config.LOGTAG,"found two candidates with same priority");
678 if (initiator.equals(account.getJid())) {
679 if (currentConnection.getCandidate().isOurs()) {
680 connection = currentConnection;
681 }
682 } else {
683 if (!currentConnection.getCandidate().isOurs()) {
684 connection = currentConnection;
685 }
686 }
687 }
688 }
689 }
690 }
691 return connection;
692 }
693
694 private void sendSuccess() {
695 JinglePacket packet = bootstrapPacket("session-terminate");
696 Reason reason = new Reason();
697 reason.addChild("success");
698 packet.setReason(reason);
699 this.sendJinglePacket(packet);
700 this.disconnectSocks5Connections();
701 this.mJingleStatus = JINGLE_STATUS_FINISHED;
702 this.message.setStatus(Message.STATUS_RECEIVED);
703 this.message.setTransferable(null);
704 this.mXmppConnectionService.updateMessage(message);
705 this.mJingleConnectionManager.finishConnection(this);
706 }
707
708 private void sendFallbackToIbb() {
709 Log.d(Config.LOGTAG, "sending fallback to ibb");
710 JinglePacket packet = this.bootstrapPacket("transport-replace");
711 Content content = new Content(this.contentCreator, this.contentName);
712 this.transportId = this.mJingleConnectionManager.nextRandomId();
713 content.setTransportId(this.transportId);
714 content.ibbTransport().setAttribute("block-size",
715 Integer.toString(this.ibbBlockSize));
716 packet.setContent(content);
717 this.sendJinglePacket(packet);
718 }
719
720 private boolean receiveFallbackToIbb(JinglePacket packet) {
721 Log.d(Config.LOGTAG, "receiving fallack to ibb");
722 String receivedBlockSize = packet.getJingleContent().ibbTransport()
723 .getAttribute("block-size");
724 if (receivedBlockSize != null) {
725 int bs = Integer.parseInt(receivedBlockSize);
726 if (bs > this.ibbBlockSize) {
727 this.ibbBlockSize = bs;
728 }
729 }
730 this.transportId = packet.getJingleContent().getTransportId();
731 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
732 this.transport.receive(file, onFileTransmissionSatusChanged);
733 JinglePacket answer = bootstrapPacket("transport-accept");
734 Content content = new Content("initiator", "a-file-offer");
735 content.setTransportId(this.transportId);
736 content.ibbTransport().setAttribute("block-size",
737 Integer.toString(this.ibbBlockSize));
738 answer.setContent(content);
739 this.sendJinglePacket(answer);
740 return true;
741 }
742
743 private boolean receiveTransportAccept(JinglePacket packet) {
744 if (packet.getJingleContent().hasIbbTransport()) {
745 String receivedBlockSize = packet.getJingleContent().ibbTransport()
746 .getAttribute("block-size");
747 if (receivedBlockSize != null) {
748 int bs = Integer.parseInt(receivedBlockSize);
749 if (bs > this.ibbBlockSize) {
750 this.ibbBlockSize = bs;
751 }
752 }
753 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
754 this.transport.connect(new OnTransportConnected() {
755
756 @Override
757 public void failed() {
758 Log.d(Config.LOGTAG, "ibb open failed");
759 }
760
761 @Override
762 public void established() {
763 JingleConnection.this.transport.send(file,
764 onFileTransmissionSatusChanged);
765 }
766 });
767 return true;
768 } else {
769 return false;
770 }
771 }
772
773 private void receiveSuccess() {
774 this.mJingleStatus = JINGLE_STATUS_FINISHED;
775 this.mXmppConnectionService.markMessage(this.message,Message.STATUS_SEND_RECEIVED);
776 this.disconnectSocks5Connections();
777 if (this.transport != null && this.transport instanceof JingleInbandTransport) {
778 this.transport.disconnect();
779 }
780 this.message.setTransferable(null);
781 this.mJingleConnectionManager.finishConnection(this);
782 }
783
784 public void cancel() {
785 this.disconnectSocks5Connections();
786 if (this.transport != null && this.transport instanceof JingleInbandTransport) {
787 this.transport.disconnect();
788 }
789 this.sendCancel();
790 this.mJingleConnectionManager.finishConnection(this);
791 if (this.responder.equals(account.getJid())) {
792 this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
793 if (this.file!=null) {
794 file.delete();
795 }
796 this.mXmppConnectionService.updateConversationUi();
797 } else {
798 this.mXmppConnectionService.markMessage(this.message,
799 Message.STATUS_SEND_FAILED);
800 this.message.setTransferable(null);
801 }
802 }
803
804 private void fail() {
805 this.mJingleStatus = JINGLE_STATUS_FAILED;
806 this.disconnectSocks5Connections();
807 if (this.transport != null && this.transport instanceof JingleInbandTransport) {
808 this.transport.disconnect();
809 }
810 FileBackend.close(mFileInputStream);
811 FileBackend.close(mFileOutputStream);
812 if (this.message != null) {
813 if (this.responder.equals(account.getJid())) {
814 this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
815 if (this.file!=null) {
816 file.delete();
817 }
818 this.mXmppConnectionService.updateConversationUi();
819 } else {
820 this.mXmppConnectionService.markMessage(this.message,
821 Message.STATUS_SEND_FAILED);
822 this.message.setTransferable(null);
823 }
824 }
825 this.mJingleConnectionManager.finishConnection(this);
826 }
827
828 private void sendCancel() {
829 JinglePacket packet = bootstrapPacket("session-terminate");
830 Reason reason = new Reason();
831 reason.addChild("cancel");
832 packet.setReason(reason);
833 this.sendJinglePacket(packet);
834 }
835
836 private void connectNextCandidate() {
837 for (JingleCandidate candidate : this.candidates) {
838 if ((!connections.containsKey(candidate.getCid()) && (!candidate
839 .isOurs()))) {
840 this.connectWithCandidate(candidate);
841 return;
842 }
843 }
844 this.sendCandidateError();
845 }
846
847 private void connectWithCandidate(final JingleCandidate candidate) {
848 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
849 this, candidate);
850 connections.put(candidate.getCid(), socksConnection);
851 socksConnection.connect(new OnTransportConnected() {
852
853 @Override
854 public void failed() {
855 Log.d(Config.LOGTAG,
856 "connection failed with " + candidate.getHost() + ":"
857 + candidate.getPort());
858 connectNextCandidate();
859 }
860
861 @Override
862 public void established() {
863 Log.d(Config.LOGTAG,
864 "established connection with " + candidate.getHost()
865 + ":" + candidate.getPort());
866 sendCandidateUsed(candidate.getCid());
867 }
868 });
869 }
870
871 private void disconnectSocks5Connections() {
872 Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
873 .entrySet().iterator();
874 while (it.hasNext()) {
875 Entry<String, JingleSocks5Transport> pairs = it.next();
876 pairs.getValue().disconnect();
877 it.remove();
878 }
879 }
880
881 private void sendProxyActivated(String cid) {
882 JinglePacket packet = bootstrapPacket("transport-info");
883 Content content = new Content(this.contentCreator, this.contentName);
884 content.setTransportId(this.transportId);
885 content.socks5transport().addChild("activated")
886 .setAttribute("cid", cid);
887 packet.setContent(content);
888 this.sendJinglePacket(packet);
889 }
890
891 private void sendCandidateUsed(final String cid) {
892 JinglePacket packet = bootstrapPacket("transport-info");
893 Content content = new Content(this.contentCreator, this.contentName);
894 content.setTransportId(this.transportId);
895 content.socks5transport().addChild("candidate-used")
896 .setAttribute("cid", cid);
897 packet.setContent(content);
898 this.sentCandidate = true;
899 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
900 connect();
901 }
902 this.sendJinglePacket(packet);
903 }
904
905 private void sendCandidateError() {
906 JinglePacket packet = bootstrapPacket("transport-info");
907 Content content = new Content(this.contentCreator, this.contentName);
908 content.setTransportId(this.transportId);
909 content.socks5transport().addChild("candidate-error");
910 packet.setContent(content);
911 this.sentCandidate = true;
912 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
913 connect();
914 }
915 this.sendJinglePacket(packet);
916 }
917
918 public Jid getInitiator() {
919 return this.initiator;
920 }
921
922 public Jid getResponder() {
923 return this.responder;
924 }
925
926 public int getJingleStatus() {
927 return this.mJingleStatus;
928 }
929
930 private boolean equalCandidateExists(JingleCandidate candidate) {
931 for (JingleCandidate c : this.candidates) {
932 if (c.equalValues(candidate)) {
933 return true;
934 }
935 }
936 return false;
937 }
938
939 private void mergeCandidate(JingleCandidate candidate) {
940 for (JingleCandidate c : this.candidates) {
941 if (c.equals(candidate)) {
942 return;
943 }
944 }
945 this.candidates.add(candidate);
946 }
947
948 private void mergeCandidates(List<JingleCandidate> candidates) {
949 for (JingleCandidate c : candidates) {
950 mergeCandidate(c);
951 }
952 }
953
954 private JingleCandidate getCandidate(String cid) {
955 for (JingleCandidate c : this.candidates) {
956 if (c.getCid().equals(cid)) {
957 return c;
958 }
959 }
960 return null;
961 }
962
963 public void updateProgress(int i) {
964 this.mProgress = i;
965 mXmppConnectionService.updateConversationUi();
966 }
967
968 interface OnProxyActivated {
969 public void success();
970
971 public void failed();
972 }
973
974 public boolean hasTransportId(String sid) {
975 return sid.equals(this.transportId);
976 }
977
978 public JingleTransport getTransport() {
979 return this.transport;
980 }
981
982 public boolean start() {
983 if (account.getStatus() == Account.State.ONLINE) {
984 if (mJingleStatus == JINGLE_STATUS_INITIATED) {
985 new Thread(new Runnable() {
986
987 @Override
988 public void run() {
989 sendAccept();
990 }
991 }).start();
992 }
993 return true;
994 } else {
995 return false;
996 }
997 }
998
999 @Override
1000 public int getStatus() {
1001 return this.mStatus;
1002 }
1003
1004 @Override
1005 public long getFileSize() {
1006 if (this.file != null) {
1007 return this.file.getExpectedSize();
1008 } else {
1009 return 0;
1010 }
1011 }
1012
1013 @Override
1014 public int getProgress() {
1015 return this.mProgress;
1016 }
1017}