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