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