JingleConnection.java

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