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