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