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