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