JingleConnection.java

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