JingleConnection.java

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