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