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