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