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