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.DownloadablePlaceholder;
 22import eu.siacs.conversations.entities.Message;
 23import eu.siacs.conversations.services.XmppConnectionService;
 24import eu.siacs.conversations.xml.Element;
 25import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 26import eu.siacs.conversations.xmpp.jid.Jid;
 27import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 28import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 29import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 30import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 31
 32public class JingleConnection implements Downloadable {
 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 = Downloadable.STATUS_UNKNOWN;
 49	private Message message;
 50	private String sessionId;
 51	private Account account;
 52	private Jid initiator;
 53	private Jid responder;
 54	private List<JingleCandidate> candidates = new ArrayList<>();
 55	private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
 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 int mProgress = 0;
 65	private long mLastGuiRefresh = 0;
 66
 67	private boolean receivedCandidate = false;
 68	private boolean sentCandidate = false;
 69
 70	private boolean acceptedAutomatically = false;
 71
 72	private JingleTransport transport = null;
 73
 74	private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
 75
 76		@Override
 77		public void onIqPacketReceived(Account account, IqPacket packet) {
 78			if (packet.getType() == IqPacket.TYPE.ERROR) {
 79				fail();
 80			}
 81		}
 82	};
 83
 84	final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() {
 85
 86		@Override
 87		public void onFileTransmitted(DownloadableFile file) {
 88			if (responder.equals(account.getJid())) {
 89				sendSuccess();
 90				if (acceptedAutomatically) {
 91					message.markUnread();
 92					JingleConnection.this.mXmppConnectionService
 93							.getNotificationService().push(message);
 94				}
 95				mXmppConnectionService.getFileBackend().updateFileParams(message);
 96				mXmppConnectionService.databaseBackend.createMessage(message);
 97				mXmppConnectionService.markMessage(message,
 98						Message.STATUS_RECEIVED);
 99			} else {
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.generateResponse(IqPacket.TYPE.RESULT);
195
196		} else {
197			response = packet.generateResponse(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).toLowerCase().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		if (this.transport != null && this.transport instanceof JingleInbandTransport) {
714			this.transport.disconnect();
715		}
716		this.message.setDownloadable(null);
717		this.mJingleConnectionManager.finishConnection(this);
718	}
719
720	public void cancel() {
721		this.disconnectSocks5Connections();
722		if (this.transport != null && this.transport instanceof JingleInbandTransport) {
723			this.transport.disconnect();
724		}
725		this.sendCancel();
726		this.mJingleConnectionManager.finishConnection(this);
727		if (this.responder.equals(account.getJid())) {
728			this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED));
729			if (this.file!=null) {
730				file.delete();
731			}
732			this.mXmppConnectionService.updateConversationUi();
733		} else {
734			this.mXmppConnectionService.markMessage(this.message,
735					Message.STATUS_SEND_FAILED);
736			this.message.setDownloadable(null);
737		}
738	}
739
740	private void fail() {
741		this.mJingleStatus = JINGLE_STATUS_FAILED;
742		this.disconnectSocks5Connections();
743		if (this.transport != null && this.transport instanceof JingleInbandTransport) {
744			this.transport.disconnect();
745		}
746		if (this.message != null) {
747			if (this.responder.equals(account.getJid())) {
748				this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED));
749				if (this.file!=null) {
750					file.delete();
751				}
752				this.mXmppConnectionService.updateConversationUi();
753			} else {
754				this.mXmppConnectionService.markMessage(this.message,
755						Message.STATUS_SEND_FAILED);
756				this.message.setDownloadable(null);
757			}
758		}
759		this.mJingleConnectionManager.finishConnection(this);
760	}
761
762	private void sendCancel() {
763		JinglePacket packet = bootstrapPacket("session-terminate");
764		Reason reason = new Reason();
765		reason.addChild("cancel");
766		packet.setReason(reason);
767		this.sendJinglePacket(packet);
768	}
769
770	private void connectNextCandidate() {
771		for (JingleCandidate candidate : this.candidates) {
772			if ((!connections.containsKey(candidate.getCid()) && (!candidate
773					.isOurs()))) {
774				this.connectWithCandidate(candidate);
775				return;
776			}
777		}
778		this.sendCandidateError();
779	}
780
781	private void connectWithCandidate(final JingleCandidate candidate) {
782		final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
783				this, candidate);
784		connections.put(candidate.getCid(), socksConnection);
785		socksConnection.connect(new OnTransportConnected() {
786
787			@Override
788			public void failed() {
789				Log.d(Config.LOGTAG,
790						"connection failed with " + candidate.getHost() + ":"
791								+ candidate.getPort());
792				connectNextCandidate();
793			}
794
795			@Override
796			public void established() {
797				Log.d(Config.LOGTAG,
798						"established connection with " + candidate.getHost()
799								+ ":" + candidate.getPort());
800				sendCandidateUsed(candidate.getCid());
801			}
802		});
803	}
804
805	private void disconnectSocks5Connections() {
806		Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
807				.entrySet().iterator();
808		while (it.hasNext()) {
809			Entry<String, JingleSocks5Transport> pairs = it.next();
810			pairs.getValue().disconnect();
811			it.remove();
812		}
813	}
814
815	private void sendProxyActivated(String cid) {
816		JinglePacket packet = bootstrapPacket("transport-info");
817		Content content = new Content(this.contentCreator, this.contentName);
818		content.setTransportId(this.transportId);
819		content.socks5transport().addChild("activated")
820				.setAttribute("cid", cid);
821		packet.setContent(content);
822		this.sendJinglePacket(packet);
823	}
824
825	private void sendCandidateUsed(final String cid) {
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-used")
830				.setAttribute("cid", cid);
831		packet.setContent(content);
832		this.sentCandidate = true;
833		if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
834			connect();
835		}
836		this.sendJinglePacket(packet);
837	}
838
839	private void sendCandidateError() {
840		JinglePacket packet = bootstrapPacket("transport-info");
841		Content content = new Content(this.contentCreator, this.contentName);
842		content.setTransportId(this.transportId);
843		content.socks5transport().addChild("candidate-error");
844		packet.setContent(content);
845		this.sentCandidate = true;
846		if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
847			connect();
848		}
849		this.sendJinglePacket(packet);
850	}
851
852	public Jid getInitiator() {
853		return this.initiator;
854	}
855
856	public Jid getResponder() {
857		return this.responder;
858	}
859
860	public int getJingleStatus() {
861		return this.mJingleStatus;
862	}
863
864	private boolean equalCandidateExists(JingleCandidate candidate) {
865		for (JingleCandidate c : this.candidates) {
866			if (c.equalValues(candidate)) {
867				return true;
868			}
869		}
870		return false;
871	}
872
873	private void mergeCandidate(JingleCandidate candidate) {
874		for (JingleCandidate c : this.candidates) {
875			if (c.equals(candidate)) {
876				return;
877			}
878		}
879		this.candidates.add(candidate);
880	}
881
882	private void mergeCandidates(List<JingleCandidate> candidates) {
883		for (JingleCandidate c : candidates) {
884			mergeCandidate(c);
885		}
886	}
887
888	private JingleCandidate getCandidate(String cid) {
889		for (JingleCandidate c : this.candidates) {
890			if (c.getCid().equals(cid)) {
891				return c;
892			}
893		}
894		return null;
895	}
896
897	public void updateProgress(int i) {
898		this.mProgress = i;
899		if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) {
900			this.mLastGuiRefresh = SystemClock.elapsedRealtime();
901			mXmppConnectionService.updateConversationUi();
902		}
903	}
904
905	interface OnProxyActivated {
906		public void success();
907
908		public void failed();
909	}
910
911	public boolean hasTransportId(String sid) {
912		return sid.equals(this.transportId);
913	}
914
915	public JingleTransport getTransport() {
916		return this.transport;
917	}
918
919	public boolean start() {
920		if (account.getStatus() == Account.State.ONLINE) {
921			if (mJingleStatus == JINGLE_STATUS_INITIATED) {
922				new Thread(new Runnable() {
923
924					@Override
925					public void run() {
926						sendAccept();
927					}
928				}).start();
929			}
930			return true;
931		} else {
932			return false;
933		}
934	}
935
936	@Override
937	public int getStatus() {
938		return this.mStatus;
939	}
940
941	@Override
942	public long getFileSize() {
943		if (this.file != null) {
944			return this.file.getExpectedSize();
945		} else {
946			return 0;
947		}
948	}
949
950	@Override
951	public int getProgress() {
952		return this.mProgress;
953	}
954
955	@Override
956	public String getMimeType() {
957		if (this.message.getType() == Message.TYPE_FILE) {
958			String mime = null;
959			String path = this.message.getRelativeFilePath();
960			if (path != null && !this.message.getRelativeFilePath().isEmpty()) {
961				mime = URLConnection.guessContentTypeFromName(this.message.getRelativeFilePath());
962				if (mime!=null) {
963					return  mime;
964				} else {
965					return "";
966				}
967			} else {
968				return "";
969			}
970		} else {
971			return "image/webp";
972		}
973	}
974}