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