JingleConnection.java

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