JingleConnection.java

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