JingleConnection.java

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