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