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