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