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