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