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