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