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