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