JingleConnection.java

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