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,Message.STATUS_SEND_RECEIVED);
711		this.disconnectSocks5Connections();
712		if (this.transport != null && this.transport instanceof JingleInbandTransport) {
713			this.transport.disconnect();
714		}
715		this.message.setDownloadable(null);
716		this.mJingleConnectionManager.finishConnection(this);
717	}
718
719	public void cancel() {
720		this.disconnectSocks5Connections();
721		if (this.transport != null && this.transport instanceof JingleInbandTransport) {
722			this.transport.disconnect();
723		}
724		this.sendCancel();
725		this.mJingleConnectionManager.finishConnection(this);
726		if (this.responder.equals(account.getJid())) {
727			this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED));
728			if (this.file!=null) {
729				file.delete();
730			}
731			this.mXmppConnectionService.updateConversationUi();
732		} else {
733			this.mXmppConnectionService.markMessage(this.message,
734					Message.STATUS_SEND_FAILED);
735			this.message.setDownloadable(null);
736		}
737	}
738
739	private void fail() {
740		this.mJingleStatus = JINGLE_STATUS_FAILED;
741		this.disconnectSocks5Connections();
742		if (this.transport != null && this.transport instanceof JingleInbandTransport) {
743			this.transport.disconnect();
744		}
745		if (this.message != null) {
746			if (this.responder.equals(account.getJid())) {
747				this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED));
748				if (this.file!=null) {
749					file.delete();
750				}
751				this.mXmppConnectionService.updateConversationUi();
752			} else {
753				this.mXmppConnectionService.markMessage(this.message,
754						Message.STATUS_SEND_FAILED);
755				this.message.setDownloadable(null);
756			}
757		}
758		this.mJingleConnectionManager.finishConnection(this);
759	}
760
761	private void sendCancel() {
762		JinglePacket packet = bootstrapPacket("session-terminate");
763		Reason reason = new Reason();
764		reason.addChild("cancel");
765		packet.setReason(reason);
766		this.sendJinglePacket(packet);
767	}
768
769	private void connectNextCandidate() {
770		for (JingleCandidate candidate : this.candidates) {
771			if ((!connections.containsKey(candidate.getCid()) && (!candidate
772					.isOurs()))) {
773				this.connectWithCandidate(candidate);
774				return;
775			}
776		}
777		this.sendCandidateError();
778	}
779
780	private void connectWithCandidate(final JingleCandidate candidate) {
781		final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
782				this, candidate);
783		connections.put(candidate.getCid(), socksConnection);
784		socksConnection.connect(new OnTransportConnected() {
785
786			@Override
787			public void failed() {
788				Log.d(Config.LOGTAG,
789						"connection failed with " + candidate.getHost() + ":"
790								+ candidate.getPort());
791				connectNextCandidate();
792			}
793
794			@Override
795			public void established() {
796				Log.d(Config.LOGTAG,
797						"established connection with " + candidate.getHost()
798								+ ":" + candidate.getPort());
799				sendCandidateUsed(candidate.getCid());
800			}
801		});
802	}
803
804	private void disconnectSocks5Connections() {
805		Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
806				.entrySet().iterator();
807		while (it.hasNext()) {
808			Entry<String, JingleSocks5Transport> pairs = it.next();
809			pairs.getValue().disconnect();
810			it.remove();
811		}
812	}
813
814	private void sendProxyActivated(String cid) {
815		JinglePacket packet = bootstrapPacket("transport-info");
816		Content content = new Content(this.contentCreator, this.contentName);
817		content.setTransportId(this.transportId);
818		content.socks5transport().addChild("activated")
819				.setAttribute("cid", cid);
820		packet.setContent(content);
821		this.sendJinglePacket(packet);
822	}
823
824	private void sendCandidateUsed(final String cid) {
825		JinglePacket packet = bootstrapPacket("transport-info");
826		Content content = new Content(this.contentCreator, this.contentName);
827		content.setTransportId(this.transportId);
828		content.socks5transport().addChild("candidate-used")
829				.setAttribute("cid", cid);
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	private void sendCandidateError() {
839		JinglePacket packet = bootstrapPacket("transport-info");
840		Content content = new Content(this.contentCreator, this.contentName);
841		content.setTransportId(this.transportId);
842		content.socks5transport().addChild("candidate-error");
843		packet.setContent(content);
844		this.sentCandidate = true;
845		if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
846			connect();
847		}
848		this.sendJinglePacket(packet);
849	}
850
851	public Jid getInitiator() {
852		return this.initiator;
853	}
854
855	public Jid getResponder() {
856		return this.responder;
857	}
858
859	public int getJingleStatus() {
860		return this.mJingleStatus;
861	}
862
863	private boolean equalCandidateExists(JingleCandidate candidate) {
864		for (JingleCandidate c : this.candidates) {
865			if (c.equalValues(candidate)) {
866				return true;
867			}
868		}
869		return false;
870	}
871
872	private void mergeCandidate(JingleCandidate candidate) {
873		for (JingleCandidate c : this.candidates) {
874			if (c.equals(candidate)) {
875				return;
876			}
877		}
878		this.candidates.add(candidate);
879	}
880
881	private void mergeCandidates(List<JingleCandidate> candidates) {
882		for (JingleCandidate c : candidates) {
883			mergeCandidate(c);
884		}
885	}
886
887	private JingleCandidate getCandidate(String cid) {
888		for (JingleCandidate c : this.candidates) {
889			if (c.getCid().equals(cid)) {
890				return c;
891			}
892		}
893		return null;
894	}
895
896	public void updateProgress(int i) {
897		this.mProgress = i;
898		if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) {
899			this.mLastGuiRefresh = SystemClock.elapsedRealtime();
900			mXmppConnectionService.updateConversationUi();
901		}
902	}
903
904	interface OnProxyActivated {
905		public void success();
906
907		public void failed();
908	}
909
910	public boolean hasTransportId(String sid) {
911		return sid.equals(this.transportId);
912	}
913
914	public JingleTransport getTransport() {
915		return this.transport;
916	}
917
918	public boolean start() {
919		if (account.getStatus() == Account.State.ONLINE) {
920			if (mJingleStatus == JINGLE_STATUS_INITIATED) {
921				new Thread(new Runnable() {
922
923					@Override
924					public void run() {
925						sendAccept();
926					}
927				}).start();
928			}
929			return true;
930		} else {
931			return false;
932		}
933	}
934
935	@Override
936	public int getStatus() {
937		return this.mStatus;
938	}
939
940	@Override
941	public long getFileSize() {
942		if (this.file != null) {
943			return this.file.getExpectedSize();
944		} else {
945			return 0;
946		}
947	}
948
949	@Override
950	public int getProgress() {
951		return this.mProgress;
952	}
953
954	@Override
955	public String getMimeType() {
956		if (this.message.getType() == Message.TYPE_FILE) {
957			String mime = null;
958			String path = this.message.getRelativeFilePath();
959			if (path != null && !this.message.getRelativeFilePath().isEmpty()) {
960				mime = URLConnection.guessContentTypeFromName(this.message.getRelativeFilePath());
961				if (mime!=null) {
962					return  mime;
963				} else {
964					return "";
965				}
966			} else {
967				return "";
968			}
969		} else {
970			return "image/webp";
971		}
972	}
973}