1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Base64;
4import android.util.Log;
5
6import java.io.File;
7import java.io.FileInputStream;
8import java.io.FileNotFoundException;
9import java.io.IOException;
10import java.io.InputStream;
11import java.io.OutputStream;
12import java.util.ArrayList;
13import java.util.Arrays;
14import java.util.Collections;
15import java.util.Iterator;
16import java.util.List;
17import java.util.Map.Entry;
18import java.util.concurrent.ConcurrentHashMap;
19
20import eu.siacs.conversations.Config;
21import eu.siacs.conversations.crypto.axolotl.AxolotlService;
22import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
23import eu.siacs.conversations.entities.Account;
24import eu.siacs.conversations.entities.Conversation;
25import eu.siacs.conversations.entities.DownloadableFile;
26import eu.siacs.conversations.entities.Message;
27import eu.siacs.conversations.entities.Presence;
28import eu.siacs.conversations.entities.ServiceDiscoveryResult;
29import eu.siacs.conversations.entities.Transferable;
30import eu.siacs.conversations.entities.TransferablePlaceholder;
31import eu.siacs.conversations.parser.IqParser;
32import eu.siacs.conversations.persistance.FileBackend;
33import eu.siacs.conversations.services.AbstractConnectionManager;
34import eu.siacs.conversations.utils.CryptoHelper;
35import eu.siacs.conversations.xml.Element;
36import eu.siacs.conversations.xml.Namespace;
37import eu.siacs.conversations.xmpp.OnIqPacketReceived;
38import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
39import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
40import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
41import eu.siacs.conversations.xmpp.stanzas.IqPacket;
42import rocks.xmpp.addr.Jid;
43
44public class JingleFileTransferConnection extends AbstractJingleConnection implements Transferable {
45
46 private static final int JINGLE_STATUS_TRANSMITTING = 5;
47 private static final String JET_OMEMO_CIPHER = "urn:xmpp:ciphers:aes-128-gcm-nopadding";
48 private static final int JINGLE_STATUS_INITIATED = 0;
49 private static final int JINGLE_STATUS_ACCEPTED = 1;
50 private static final int JINGLE_STATUS_FINISHED = 4;
51 private static final int JINGLE_STATUS_FAILED = 99;
52 private static final int JINGLE_STATUS_OFFERED = -1;
53 private Content.Version ftVersion = Content.Version.FT_3;
54
55 private int ibbBlockSize = 8192;
56
57 private int mJingleStatus = JINGLE_STATUS_OFFERED; //migrate to enum
58 private int mStatus = Transferable.STATUS_UNKNOWN;
59 private Message message;
60 private Jid initiator;
61 private Jid responder;
62 private List<JingleCandidate> candidates = new ArrayList<>();
63 private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
64
65 private String transportId;
66 private Element fileOffer;
67 private DownloadableFile file = null;
68
69 private boolean proxyActivationFailed = false;
70
71 private String contentName;
72 private Content.Creator contentCreator;
73 private Transport initialTransport;
74 private boolean remoteSupportsOmemoJet;
75
76 private int mProgress = 0;
77
78 private boolean receivedCandidate = false;
79 private boolean sentCandidate = false;
80
81 private boolean acceptedAutomatically = false;
82 private boolean cancelled = false;
83
84 private XmppAxolotlMessage mXmppAxolotlMessage;
85
86 private JingleTransport transport = null;
87
88 private OutputStream mFileOutputStream;
89 private InputStream mFileInputStream;
90
91 private OnIqPacketReceived responseListener = (account, packet) -> {
92 if (packet.getType() != IqPacket.TYPE.RESULT) {
93 if (mJingleStatus != JINGLE_STATUS_FAILED && mJingleStatus != JINGLE_STATUS_FINISHED) {
94 fail(IqParser.extractErrorMessage(packet));
95 } else {
96 Log.d(Config.LOGTAG, "ignoring late delivery of jingle packet to jingle session with status=" + mJingleStatus + ": " + packet.toString());
97 }
98 }
99 };
100 private byte[] expectedHash = new byte[0];
101 private final OnFileTransmissionStatusChanged onFileTransmissionStatusChanged = new OnFileTransmissionStatusChanged() {
102
103 @Override
104 public void onFileTransmitted(DownloadableFile file) {
105 if (responding()) {
106 if (expectedHash.length > 0) {
107 if (Arrays.equals(expectedHash, file.getSha1Sum())) {
108 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received file matched the expected hash");
109 } else {
110 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": hashes did not match");
111 }
112 } else {
113 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party did not include file hash in file transfer");
114 }
115 sendSuccess();
116 xmppConnectionService.getFileBackend().updateFileParams(message);
117 xmppConnectionService.databaseBackend.createMessage(message);
118 xmppConnectionService.markMessage(message, Message.STATUS_RECEIVED);
119 if (acceptedAutomatically) {
120 message.markUnread();
121 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
122 id.account.getPgpDecryptionService().decrypt(message, true);
123 } else {
124 xmppConnectionService.getFileBackend().updateMediaScanner(file, () -> JingleFileTransferConnection.this.xmppConnectionService.getNotificationService().push(message));
125
126 }
127 Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
128 return;
129 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
130 id.account.getPgpDecryptionService().decrypt(message, true);
131 }
132 } else {
133 if (ftVersion == Content.Version.FT_5) { //older Conversations will break when receiving a session-info
134 sendHash();
135 }
136 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
137 id.account.getPgpDecryptionService().decrypt(message, false);
138 }
139 if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
140 file.delete();
141 }
142 }
143 Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
144 if (message.getEncryption() != Message.ENCRYPTION_PGP) {
145 xmppConnectionService.getFileBackend().updateMediaScanner(file);
146 }
147 }
148
149 @Override
150 public void onFileTransferAborted() {
151 JingleFileTransferConnection.this.sendSessionTerminate("connectivity-error");
152 JingleFileTransferConnection.this.fail();
153 }
154 };
155 private OnTransportConnected onIbbTransportConnected = new OnTransportConnected() {
156 @Override
157 public void failed() {
158 Log.d(Config.LOGTAG, "ibb open failed");
159 }
160
161 @Override
162 public void established() {
163 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ibb transport connected. sending file");
164 mJingleStatus = JINGLE_STATUS_TRANSMITTING;
165 JingleFileTransferConnection.this.transport.send(file, onFileTransmissionStatusChanged);
166 }
167 };
168 private OnProxyActivated onProxyActivated = new OnProxyActivated() {
169
170 @Override
171 public void success() {
172 if (initiator.equals(id.account.getJid())) {
173 Log.d(Config.LOGTAG, "we were initiating. sending file");
174 transport.send(file, onFileTransmissionStatusChanged);
175 } else {
176 transport.receive(file, onFileTransmissionStatusChanged);
177 Log.d(Config.LOGTAG, "we were responding. receiving file");
178 }
179 }
180
181 @Override
182 public void failed() {
183 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": proxy activation failed");
184 proxyActivationFailed = true;
185 if (initiating()) {
186 sendFallbackToIbb();
187 }
188 }
189 };
190
191 public JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id) {
192 super(jingleConnectionManager, id);
193 }
194
195 private static long parseLong(final Element element, final long l) {
196 final String input = element == null ? null : element.getContent();
197 if (input == null) {
198 return l;
199 }
200 try {
201 return Long.parseLong(input);
202 } catch (Exception e) {
203 return l;
204 }
205 }
206
207 private boolean responding() {
208 return responder != null && responder.equals(id.account.getJid());
209 }
210
211 private boolean initiating() {
212 return initiator.equals(id.account.getJid());
213 }
214
215 InputStream getFileInputStream() {
216 return this.mFileInputStream;
217 }
218
219 OutputStream getFileOutputStream() throws IOException {
220 if (this.file == null) {
221 Log.d(Config.LOGTAG, "file object was not assigned");
222 return null;
223 }
224 final File parent = this.file.getParentFile();
225 if (parent != null && parent.mkdirs()) {
226 Log.d(Config.LOGTAG, "created parent directories for file " + file.getAbsolutePath());
227 }
228 if (this.file.createNewFile()) {
229 Log.d(Config.LOGTAG, "created output file " + file.getAbsolutePath());
230 }
231 this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file, false, true);
232 return this.mFileOutputStream;
233 }
234
235 @Override
236 void deliverPacket(final JinglePacket packet) {
237 final JinglePacket.Action action = packet.getAction();
238 //TODO switch case
239 if (action == JinglePacket.Action.SESSION_TERMINATE) {
240 Reason reason = packet.getReason();
241 if (reason != null) {
242 if (reason.hasChild("cancel")) {
243 this.cancelled = true;
244 this.fail();
245 } else if (reason.hasChild("success")) {
246 this.receiveSuccess();
247 } else {
248 final List<Element> children = reason.getChildren();
249 if (children.size() == 1) {
250 this.fail(children.get(0).getName());
251 } else {
252 this.fail();
253 }
254 }
255 } else {
256 this.fail();
257 }
258 } else if (action == JinglePacket.Action.SESSION_ACCEPT) {
259 receiveAccept(packet);
260 } else if (action == JinglePacket.Action.SESSION_INFO) {
261 final Element checksum = packet.getJingleChild("checksum");
262 final Element file = checksum == null ? null : checksum.findChild("file");
263 final Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2");
264 if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) {
265 try {
266 this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT);
267 } catch (Exception e) {
268 this.expectedHash = new byte[0];
269 }
270 }
271 respondToIq(packet, true);
272 } else if (action == JinglePacket.Action.TRANSPORT_INFO) {
273 receiveTransportInfo(packet);
274 } else if (action == JinglePacket.Action.TRANSPORT_REPLACE) {
275 if (packet.getJingleContent().hasIbbTransport()) {
276 receiveFallbackToIbb(packet);
277 } else {
278 Log.d(Config.LOGTAG, "trying to fallback to something unknown" + packet.toString());
279 respondToIq(packet, false);
280 }
281 } else if (action == JinglePacket.Action.TRANSPORT_ACCEPT) {
282 receiveTransportAccept(packet);
283 } else {
284 Log.d(Config.LOGTAG, "packet arrived in connection. action was " + packet.getAction());
285 respondToIq(packet, false);
286 }
287 }
288
289 private void respondToIq(final IqPacket packet, final boolean result) {
290 final IqPacket response;
291 if (result) {
292 response = packet.generateResponse(IqPacket.TYPE.RESULT);
293 } else {
294 response = packet.generateResponse(IqPacket.TYPE.ERROR);
295 final Element error = response.addChild("error").setAttribute("type", "cancel");
296 error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas");
297 }
298 xmppConnectionService.sendIqPacket(id.account, response, null);
299 }
300
301 private void respondToIqWithOutOfOrder(final IqPacket packet) {
302 final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
303 final Element error = response.addChild("error").setAttribute("type", "wait");
304 error.addChild("unexpected-request", "urn:ietf:params:xml:ns:xmpp-stanzas");
305 error.addChild("out-of-order", "urn:xmpp:jingle:errors:1");
306 xmppConnectionService.sendIqPacket(id.account, response, null);
307 }
308
309 public void init(final Message message) {
310 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
311 Conversation conversation = (Conversation) message.getConversation();
312 conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, xmppAxolotlMessage -> {
313 if (xmppAxolotlMessage != null) {
314 init(message, xmppAxolotlMessage);
315 } else {
316 fail();
317 }
318 });
319 } else {
320 init(message, null);
321 }
322 }
323
324 private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
325 this.mXmppAxolotlMessage = xmppAxolotlMessage;
326 this.contentCreator = Content.Creator.INITIATOR;
327 this.contentName = JingleConnectionManager.nextRandomId();
328 this.message = message;
329 final List<String> remoteFeatures = getRemoteFeatures();
330 upgradeNamespace(remoteFeatures);
331 this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? Transport.SOCKS : Transport.IBB;
332 this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO);
333 this.message.setTransferable(this);
334 this.mStatus = Transferable.STATUS_UPLOADING;
335 this.initiator = this.id.account.getJid();
336 this.responder = this.id.counterPart;
337 this.transportId = JingleConnectionManager.nextRandomId();
338 if (this.initialTransport == Transport.IBB) {
339 this.sendInitRequest();
340 } else {
341 gatherAndConnectDirectCandidates();
342 this.jingleConnectionManager.getPrimaryCandidate(id.account, initiating(), (success, candidate) -> {
343 if (success) {
344 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
345 connections.put(candidate.getCid(), socksConnection);
346 socksConnection.connect(new OnTransportConnected() {
347
348 @Override
349 public void failed() {
350 Log.d(Config.LOGTAG, String.format("connection to our own proxy65 candidate failed (%s:%d)", candidate.getHost(), candidate.getPort()));
351 sendInitRequest();
352 }
353
354 @Override
355 public void established() {
356 Log.d(Config.LOGTAG, "successfully connected to our own proxy65 candidate");
357 mergeCandidate(candidate);
358 sendInitRequest();
359 }
360 });
361 mergeCandidate(candidate);
362 } else {
363 Log.d(Config.LOGTAG, "no proxy65 candidate of our own was found");
364 sendInitRequest();
365 }
366 });
367 }
368
369 }
370
371 private void gatherAndConnectDirectCandidates() {
372 final List<JingleCandidate> directCandidates;
373 if (Config.USE_DIRECT_JINGLE_CANDIDATES) {
374 if (id.account.isOnion() || xmppConnectionService.useTorToConnect()) {
375 directCandidates = Collections.emptyList();
376 } else {
377 directCandidates = DirectConnectionUtils.getLocalCandidates(id.account.getJid());
378 }
379 } else {
380 directCandidates = Collections.emptyList();
381 }
382 for (JingleCandidate directCandidate : directCandidates) {
383 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, directCandidate);
384 connections.put(directCandidate.getCid(), socksConnection);
385 candidates.add(directCandidate);
386 }
387 }
388
389 private void upgradeNamespace(List<String> remoteFeatures) {
390 if (remoteFeatures.contains(Content.Version.FT_5.getNamespace())) {
391 this.ftVersion = Content.Version.FT_5;
392 } else if (remoteFeatures.contains(Content.Version.FT_4.getNamespace())) {
393 this.ftVersion = Content.Version.FT_4;
394 }
395 }
396
397 private List<String> getRemoteFeatures() {
398 final Jid jid = this.id.counterPart;
399 String resource = jid != null ? jid.getResource() : null;
400 if (resource != null) {
401 Presence presence = this.id.account.getRoster().getContact(jid).getPresences().getPresences().get(resource);
402 ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
403 return result == null ? Collections.emptyList() : result.getFeatures();
404 } else {
405 return Collections.emptyList();
406 }
407 }
408
409 public void init(Account account, JinglePacket packet) { //should move to deliverPacket
410 this.mJingleStatus = JINGLE_STATUS_INITIATED;
411 Conversation conversation = this.xmppConnectionService
412 .findOrCreateConversation(account,
413 packet.getFrom().asBareJid(), false, false);
414 this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
415 this.message.setStatus(Message.STATUS_RECEIVED);
416 this.mStatus = Transferable.STATUS_OFFER;
417 this.message.setTransferable(this);
418 this.message.setCounterpart(this.id.counterPart);
419 this.initiator = this.id.counterPart;
420 this.responder = this.id.account.getJid();
421 final Content content = packet.getJingleContent();
422 this.contentCreator = content.getCreator();
423 this.initialTransport = content.hasSocks5Transport() ? Transport.SOCKS : Transport.IBB;
424 this.contentName = content.getAttribute("name");
425 this.transportId = content.getTransportId();
426
427
428 if (this.initialTransport == Transport.SOCKS) {
429 this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
430 } else if (this.initialTransport == Transport.IBB) {
431 final String receivedBlockSize = content.ibbTransport().getAttribute("block-size");
432 if (receivedBlockSize != null) {
433 try {
434 this.ibbBlockSize = Math.min(Integer.parseInt(receivedBlockSize), this.ibbBlockSize);
435 } catch (NumberFormatException e) {
436 Log.d(Config.LOGTAG, "number format exception " + e.getMessage());
437 respondToIq(packet, false);
438 this.fail();
439 return;
440 }
441 } else {
442 Log.d(Config.LOGTAG, "received block size was null");
443 respondToIq(packet, false);
444 this.fail();
445 return;
446 }
447 }
448 this.ftVersion = content.getVersion();
449 if (ftVersion == null) {
450 respondToIq(packet, false);
451 this.fail();
452 return;
453 }
454 this.fileOffer = content.getFileOffer(this.ftVersion);
455
456
457 if (fileOffer != null) {
458 boolean remoteIsUsingJet = false;
459 Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
460 if (encrypted == null) {
461 final Element security = content.findChild("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
462 if (security != null && AxolotlService.PEP_PREFIX.equals(security.getAttribute("type"))) {
463 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received jingle file offer with JET");
464 encrypted = security.findChild("encrypted", AxolotlService.PEP_PREFIX);
465 remoteIsUsingJet = true;
466 }
467 }
468 if (encrypted != null) {
469 this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid());
470 }
471 Element fileSize = fileOffer.findChild("size");
472 final String path = fileOffer.findChildContent("name");
473 if (path != null) {
474 AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(path);
475 if (VALID_IMAGE_EXTENSIONS.contains(extension.main)) {
476 message.setType(Message.TYPE_IMAGE);
477 message.setRelativeFilePath(message.getUuid() + "." + extension.main);
478 } else if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
479 if (VALID_IMAGE_EXTENSIONS.contains(extension.secondary)) {
480 message.setType(Message.TYPE_IMAGE);
481 message.setRelativeFilePath(message.getUuid() + "." + extension.secondary);
482 } else {
483 message.setType(Message.TYPE_FILE);
484 message.setRelativeFilePath(message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : ""));
485 }
486 message.setEncryption(Message.ENCRYPTION_PGP);
487 } else {
488 message.setType(Message.TYPE_FILE);
489 message.setRelativeFilePath(message.getUuid() + (extension.main != null ? ("." + extension.main) : ""));
490 }
491 long size = parseLong(fileSize, 0);
492 message.setBody(Long.toString(size));
493 conversation.add(message);
494 jingleConnectionManager.updateConversationUi(true);
495 this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
496 if (mXmppAxolotlMessage != null) {
497 XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = id.account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage, false);
498 if (transportMessage != null) {
499 message.setEncryption(Message.ENCRYPTION_AXOLOTL);
500 this.file.setKey(transportMessage.getKey());
501 this.file.setIv(transportMessage.getIv());
502 message.setFingerprint(transportMessage.getFingerprint());
503 } else {
504 Log.d(Config.LOGTAG, "could not process KeyTransportMessage");
505 }
506 }
507 message.resetFileParams();
508 //legacy OMEMO encrypted file transfers reported the file size after encryption
509 //JET reports the plain text size. however lower levels of our receiving code still
510 //expect the cipher text size. so we just + 16 bytes (auth tag size) here
511 this.file.setExpectedSize(size + (remoteIsUsingJet ? 16 : 0));
512
513 respondToIq(packet, true);
514
515 if (id.account.getRoster().getContact(id.counterPart).showInContactList()
516 && jingleConnectionManager.hasStoragePermission()
517 && size < this.jingleConnectionManager.getAutoAcceptFileSize()
518 && xmppConnectionService.isDataSaverDisabled()) {
519 Log.d(Config.LOGTAG, "auto accepting file from " + id.counterPart);
520 this.acceptedAutomatically = true;
521 this.sendAccept();
522 } else {
523 message.markUnread();
524 Log.d(Config.LOGTAG,
525 "not auto accepting new file offer with size: "
526 + size
527 + " allowed size:"
528 + this.jingleConnectionManager
529 .getAutoAcceptFileSize());
530 this.xmppConnectionService.getNotificationService().push(message);
531 }
532 Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
533 return;
534 }
535 respondToIq(packet, false);
536 }
537 }
538
539 private void sendInitRequest() {
540 final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE);
541 final Content content = new Content(this.contentCreator, this.contentName);
542 if (message.isFileOrImage()) {
543 content.setTransportId(this.transportId);
544 this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
545 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
546 this.file.setKey(mXmppAxolotlMessage.getInnerKey());
547 this.file.setIv(mXmppAxolotlMessage.getIV());
548 //legacy OMEMO encrypted file transfer reported file size of the encrypted file
549 //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag)
550 this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16));
551 final Element file = content.setFileOffer(this.file, false, this.ftVersion);
552 if (remoteSupportsOmemoJet) {
553 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET");
554 final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
555 security.setAttribute("name", this.contentName);
556 security.setAttribute("cipher", JET_OMEMO_CIPHER);
557 security.setAttribute("type", AxolotlService.PEP_PREFIX);
558 security.addChild(mXmppAxolotlMessage.toElement());
559 content.addChild(security);
560 } else {
561 file.addChild(mXmppAxolotlMessage.toElement());
562 }
563 } else {
564 this.file.setExpectedSize(file.getSize());
565 content.setFileOffer(this.file, false, this.ftVersion);
566 }
567 message.resetFileParams();
568 try {
569 this.mFileInputStream = new FileInputStream(file);
570 } catch (FileNotFoundException e) {
571 fail(e.getMessage());
572 return;
573 }
574 content.setTransportId(this.transportId);
575 if (this.initialTransport == Transport.IBB) {
576 content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize));
577 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer");
578 } else {
579 final List<Element> candidates = getCandidatesAsElements();
580 Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size()));
581 content.socks5transport().setChildren(candidates);
582 }
583 packet.setJingleContent(content);
584 this.sendJinglePacket(packet, (account, response) -> {
585 if (response.getType() == IqPacket.TYPE.RESULT) {
586 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer");
587 if (mJingleStatus == JINGLE_STATUS_OFFERED) {
588 mJingleStatus = JINGLE_STATUS_INITIATED;
589 xmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
590 } else {
591 Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus);
592 }
593 } else {
594 fail(IqParser.extractErrorMessage(response));
595 }
596 });
597
598 }
599 }
600
601 private void sendHash() {
602
603 final Element checksum = new Element("checksum", ftVersion.getNamespace());
604 checksum.setAttribute("creator", "initiator");
605 checksum.setAttribute("name", "a-file-offer");
606 Element hash = checksum.addChild("file").addChild("hash", "urn:xmpp:hashes:2");
607 hash.setAttribute("algo", "sha-1").setContent(Base64.encodeToString(file.getSha1Sum(), Base64.NO_WRAP));
608
609 final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INFO);
610 packet.setJingleChild(checksum);
611 this.sendJinglePacket(packet);
612 }
613
614 private List<Element> getCandidatesAsElements() {
615 List<Element> elements = new ArrayList<>();
616 for (JingleCandidate c : this.candidates) {
617 if (c.isOurs()) {
618 elements.add(c.toElement());
619 }
620 }
621 return elements;
622 }
623
624 private void sendAccept() {
625 mJingleStatus = JINGLE_STATUS_ACCEPTED;
626 this.mStatus = Transferable.STATUS_DOWNLOADING;
627 this.jingleConnectionManager.updateConversationUi(true);
628 if (initialTransport == Transport.SOCKS) {
629 sendAcceptSocks();
630 } else {
631 sendAcceptIbb();
632 }
633 }
634
635 private void sendAcceptSocks() {
636 gatherAndConnectDirectCandidates();
637 this.jingleConnectionManager.getPrimaryCandidate(this.id.account, initiating(), (success, candidate) -> {
638 final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
639 final Content content = new Content(contentCreator, contentName);
640 content.setFileOffer(fileOffer, ftVersion);
641 content.setTransportId(transportId);
642 if (success && candidate != null && !equalCandidateExists(candidate)) {
643 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
644 connections.put(candidate.getCid(), socksConnection);
645 socksConnection.connect(new OnTransportConnected() {
646
647 @Override
648 public void failed() {
649 Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed");
650 content.socks5transport().setChildren(getCandidatesAsElements());
651 packet.setJingleContent(content);
652 sendJinglePacket(packet);
653 connectNextCandidate();
654 }
655
656 @Override
657 public void established() {
658 Log.d(Config.LOGTAG, "connected to proxy65 candidate");
659 mergeCandidate(candidate);
660 content.socks5transport().setChildren(getCandidatesAsElements());
661 packet.setJingleContent(content);
662 sendJinglePacket(packet);
663 connectNextCandidate();
664 }
665 });
666 } else {
667 Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves");
668 content.socks5transport().setChildren(getCandidatesAsElements());
669 packet.setJingleContent(content);
670 sendJinglePacket(packet);
671 connectNextCandidate();
672 }
673 });
674 }
675
676 private void sendAcceptIbb() {
677 this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
678 final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
679 final Content content = new Content(contentCreator, contentName);
680 content.setFileOffer(fileOffer, ftVersion);
681 content.setTransportId(transportId);
682 content.ibbTransport().setAttribute("block-size", this.ibbBlockSize);
683 packet.setJingleContent(content);
684 this.transport.receive(file, onFileTransmissionStatusChanged);
685 this.sendJinglePacket(packet);
686 }
687
688 private JinglePacket bootstrapPacket(JinglePacket.Action action) {
689 final JinglePacket packet = new JinglePacket(action, this.id.sessionId);
690 packet.setTo(id.counterPart);
691 return packet;
692 }
693
694 private void sendJinglePacket(JinglePacket packet) {
695 xmppConnectionService.sendIqPacket(id.account, packet, responseListener);
696 }
697
698 private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
699 xmppConnectionService.sendIqPacket(id.account, packet, callback);
700 }
701
702 private void receiveAccept(JinglePacket packet) {
703 if (responding()) {
704 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept (we were responding)");
705 respondToIqWithOutOfOrder(packet);
706 return;
707 }
708 if (this.mJingleStatus != JINGLE_STATUS_INITIATED) {
709 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept");
710 respondToIqWithOutOfOrder(packet);
711 return;
712 }
713 this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
714 xmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
715 Content content = packet.getJingleContent();
716 if (content.hasSocks5Transport()) {
717 respondToIq(packet, true);
718 mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
719 this.connectNextCandidate();
720 } else if (content.hasIbbTransport()) {
721 String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
722 if (receivedBlockSize != null) {
723 try {
724 int bs = Integer.parseInt(receivedBlockSize);
725 if (bs > this.ibbBlockSize) {
726 this.ibbBlockSize = bs;
727 }
728 } catch (Exception e) {
729 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in session-accept");
730 }
731 }
732 respondToIq(packet, true);
733 this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
734 this.transport.connect(onIbbTransportConnected);
735 } else {
736 respondToIq(packet, false);
737 }
738 }
739
740 private void receiveTransportInfo(JinglePacket packet) {
741 final Content content = packet.getJingleContent();
742 if (content.hasSocks5Transport()) {
743 if (content.socks5transport().hasChild("activated")) {
744 respondToIq(packet, true);
745 if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
746 onProxyActivated.success();
747 } else {
748 String cid = content.socks5transport().findChild("activated").getAttribute("cid");
749 Log.d(Config.LOGTAG, "received proxy activated (" + cid
750 + ")prior to choosing our own transport");
751 JingleSocks5Transport connection = this.connections.get(cid);
752 if (connection != null) {
753 connection.setActivated(true);
754 } else {
755 Log.d(Config.LOGTAG, "activated connection not found");
756 sendSessionTerminate("failed-transport");
757 this.fail();
758 }
759 }
760 } else if (content.socks5transport().hasChild("proxy-error")) {
761 respondToIq(packet, true);
762 onProxyActivated.failed();
763 } else if (content.socks5transport().hasChild("candidate-error")) {
764 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received candidate error");
765 respondToIq(packet, true);
766 this.receivedCandidate = true;
767 if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
768 this.connect();
769 }
770 } else if (content.socks5transport().hasChild("candidate-used")) {
771 String cid = content.socks5transport().findChild("candidate-used").getAttribute("cid");
772 if (cid != null) {
773 Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
774 JingleCandidate candidate = getCandidate(cid);
775 if (candidate == null) {
776 Log.d(Config.LOGTAG, "could not find candidate with cid=" + cid);
777 respondToIq(packet, false);
778 return;
779 }
780 respondToIq(packet, true);
781 candidate.flagAsUsedByCounterpart();
782 this.receivedCandidate = true;
783 if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
784 this.connect();
785 } else {
786 Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status=" + mJingleStatus + " sentCandidate=" + sentCandidate);
787 }
788 } else {
789 respondToIq(packet, false);
790 }
791 } else {
792 respondToIq(packet, false);
793 }
794 } else {
795 respondToIq(packet, true);
796 }
797 }
798
799 private void connect() {
800 final JingleSocks5Transport connection = chooseConnection();
801 this.transport = connection;
802 if (connection == null) {
803 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": could not find suitable candidate");
804 this.disconnectSocks5Connections();
805 if (initiating()) {
806 this.sendFallbackToIbb();
807 }
808 } else {
809 final JingleCandidate candidate = connection.getCandidate();
810 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.getHost() + ":" + candidate.getPort());
811 this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
812 if (connection.needsActivation()) {
813 if (connection.getCandidate().isOurs()) {
814 final String sid;
815 if (ftVersion == Content.Version.FT_3) {
816 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy");
817 sid = id.sessionId;
818 } else {
819 sid = getTransportId();
820 }
821 Log.d(Config.LOGTAG, "candidate "
822 + connection.getCandidate().getCid()
823 + " was our proxy. going to activate");
824 IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
825 activation.setTo(connection.getCandidate().getJid());
826 activation.query("http://jabber.org/protocol/bytestreams")
827 .setAttribute("sid", sid);
828 activation.query().addChild("activate")
829 .setContent(this.id.counterPart.toEscapedString());
830 xmppConnectionService.sendIqPacket(this.id.account, activation, (account, response) -> {
831 if (response.getType() != IqPacket.TYPE.RESULT) {
832 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": " + response.toString());
833 sendProxyError();
834 onProxyActivated.failed();
835 } else {
836 sendProxyActivated(connection.getCandidate().getCid());
837 onProxyActivated.success();
838 }
839 });
840 } else {
841 Log.d(Config.LOGTAG,
842 "candidate "
843 + connection.getCandidate().getCid()
844 + " was a proxy. waiting for other party to activate");
845 }
846 } else {
847 if (initiating()) {
848 Log.d(Config.LOGTAG, "we were initiating. sending file");
849 connection.send(file, onFileTransmissionStatusChanged);
850 } else {
851 Log.d(Config.LOGTAG, "we were responding. receiving file");
852 connection.receive(file, onFileTransmissionStatusChanged);
853 }
854 }
855 }
856 }
857
858 private JingleSocks5Transport chooseConnection() {
859 JingleSocks5Transport connection = null;
860 for (Entry<String, JingleSocks5Transport> cursor : connections
861 .entrySet()) {
862 JingleSocks5Transport currentConnection = cursor.getValue();
863 // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
864 if (currentConnection.isEstablished()
865 && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
866 .getCandidate().isOurs()))) {
867 // Log.d(Config.LOGTAG,"is usable");
868 if (connection == null) {
869 connection = currentConnection;
870 } else {
871 if (connection.getCandidate().getPriority() < currentConnection
872 .getCandidate().getPriority()) {
873 connection = currentConnection;
874 } else if (connection.getCandidate().getPriority() == currentConnection
875 .getCandidate().getPriority()) {
876 // Log.d(Config.LOGTAG,"found two candidates with same priority");
877 if (initiating()) {
878 if (currentConnection.getCandidate().isOurs()) {
879 connection = currentConnection;
880 }
881 } else {
882 if (!currentConnection.getCandidate().isOurs()) {
883 connection = currentConnection;
884 }
885 }
886 }
887 }
888 }
889 }
890 return connection;
891 }
892
893 private void sendSuccess() {
894 sendSessionTerminate("success");
895 this.disconnectSocks5Connections();
896 this.mJingleStatus = JINGLE_STATUS_FINISHED;
897 this.message.setStatus(Message.STATUS_RECEIVED);
898 this.message.setTransferable(null);
899 this.xmppConnectionService.updateMessage(message, false);
900 this.jingleConnectionManager.finishConnection(this);
901 }
902
903 private void sendFallbackToIbb() {
904 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb");
905 JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE);
906 Content content = new Content(this.contentCreator, this.contentName);
907 this.transportId = JingleConnectionManager.nextRandomId();
908 content.setTransportId(this.transportId);
909 content.ibbTransport().setAttribute("block-size",
910 Integer.toString(this.ibbBlockSize));
911 packet.setJingleContent(content);
912 this.sendJinglePacket(packet);
913 }
914
915
916 private void receiveFallbackToIbb(JinglePacket packet) {
917 if (initiating()) {
918 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace (we were initiating)");
919 respondToIqWithOutOfOrder(packet);
920 return;
921 }
922 final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING);
923 if (!validState) {
924 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace");
925 respondToIqWithOutOfOrder(packet);
926 return;
927 }
928 this.proxyActivationFailed = false; //fallback received; now we no longer need to accept another one;
929 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receiving fallback to ibb");
930 final String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
931 if (receivedBlockSize != null) {
932 try {
933 final int bs = Integer.parseInt(receivedBlockSize);
934 if (bs < this.ibbBlockSize) {
935 this.ibbBlockSize = bs;
936 }
937 } catch (NumberFormatException e) {
938 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-replace");
939 }
940 }
941 this.transportId = packet.getJingleContent().getTransportId();
942 this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
943
944 final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT);
945
946 final Content content = new Content(contentCreator, contentName);
947 content.ibbTransport().setAttribute("block-size", this.ibbBlockSize);
948 content.ibbTransport().setAttribute("sid", this.transportId);
949 answer.setJingleContent(content);
950
951 respondToIq(packet, true);
952
953 if (initiating()) {
954 this.sendJinglePacket(answer, (account, response) -> {
955 if (response.getType() == IqPacket.TYPE.RESULT) {
956 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb");
957 transport.connect(onIbbTransportConnected);
958 }
959 });
960 } else {
961 this.transport.receive(file, onFileTransmissionStatusChanged);
962 this.sendJinglePacket(answer);
963 }
964 }
965
966 private void receiveTransportAccept(JinglePacket packet) {
967 if (responding()) {
968 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept (we were responding)");
969 respondToIqWithOutOfOrder(packet);
970 return;
971 }
972 final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING);
973 if (!validState) {
974 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept");
975 respondToIqWithOutOfOrder(packet);
976 return;
977 }
978 this.proxyActivationFailed = false; //fallback accepted; now we no longer need to accept another one;
979 if (packet.getJingleContent().hasIbbTransport()) {
980 final Element ibbTransport = packet.getJingleContent().ibbTransport();
981 final String receivedBlockSize = ibbTransport.getAttribute("block-size");
982 final String sid = ibbTransport.getAttribute("sid");
983 if (receivedBlockSize != null) {
984 try {
985 int bs = Integer.parseInt(receivedBlockSize);
986 if (bs < this.ibbBlockSize) {
987 this.ibbBlockSize = bs;
988 }
989 } catch (NumberFormatException e) {
990 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-accept");
991 }
992 }
993 this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
994
995 if (sid == null || !sid.equals(this.transportId)) {
996 Log.w(Config.LOGTAG, String.format("%s: sid in transport-accept (%s) did not match our sid (%s) ", id.account.getJid().asBareJid(), sid, transportId));
997 }
998 respondToIq(packet, true);
999 //might be receive instead if we are not initiating
1000 if (initiating()) {
1001 this.transport.connect(onIbbTransportConnected);
1002 }
1003 } else {
1004 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received invalid transport-accept");
1005 respondToIq(packet, false);
1006 }
1007 }
1008
1009 private void receiveSuccess() {
1010 if (initiating()) {
1011 this.mJingleStatus = JINGLE_STATUS_FINISHED;
1012 this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED);
1013 this.disconnectSocks5Connections();
1014 if (this.transport instanceof JingleInBandTransport) {
1015 this.transport.disconnect();
1016 }
1017 this.message.setTransferable(null);
1018 this.jingleConnectionManager.finishConnection(this);
1019 } else {
1020 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate/success while responding");
1021 }
1022 }
1023
1024 @Override
1025 public void cancel() {
1026 this.cancelled = true;
1027 abort("cancel");
1028 }
1029
1030 void abort(final String reason) {
1031 this.disconnectSocks5Connections();
1032 if (this.transport instanceof JingleInBandTransport) {
1033 this.transport.disconnect();
1034 }
1035 sendSessionTerminate(reason);
1036 this.jingleConnectionManager.finishConnection(this);
1037 if (responding()) {
1038 this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
1039 if (this.file != null) {
1040 file.delete();
1041 }
1042 this.jingleConnectionManager.updateConversationUi(true);
1043 } else {
1044 this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null);
1045 this.message.setTransferable(null);
1046 }
1047 }
1048
1049 private void fail() {
1050 fail(null);
1051 }
1052
1053 private void fail(String errorMessage) {
1054 this.mJingleStatus = JINGLE_STATUS_FAILED;
1055 this.disconnectSocks5Connections();
1056 if (this.transport instanceof JingleInBandTransport) {
1057 this.transport.disconnect();
1058 }
1059 FileBackend.close(mFileInputStream);
1060 FileBackend.close(mFileOutputStream);
1061 if (this.message != null) {
1062 if (responding()) {
1063 this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
1064 if (this.file != null) {
1065 file.delete();
1066 }
1067 this.jingleConnectionManager.updateConversationUi(true);
1068 } else {
1069 this.xmppConnectionService.markMessage(this.message,
1070 Message.STATUS_SEND_FAILED,
1071 cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
1072 this.message.setTransferable(null);
1073 }
1074 }
1075 this.jingleConnectionManager.finishConnection(this);
1076 }
1077
1078 private void sendSessionTerminate(String reason) {
1079 final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_TERMINATE);
1080 final Reason r = new Reason();
1081 r.addChild(reason);
1082 packet.setReason(r);
1083 this.sendJinglePacket(packet);
1084 }
1085
1086 private void connectNextCandidate() {
1087 for (JingleCandidate candidate : this.candidates) {
1088 if ((!connections.containsKey(candidate.getCid()) && (!candidate
1089 .isOurs()))) {
1090 this.connectWithCandidate(candidate);
1091 return;
1092 }
1093 }
1094 this.sendCandidateError();
1095 }
1096
1097 private void connectWithCandidate(final JingleCandidate candidate) {
1098 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
1099 this, candidate);
1100 connections.put(candidate.getCid(), socksConnection);
1101 socksConnection.connect(new OnTransportConnected() {
1102
1103 @Override
1104 public void failed() {
1105 Log.d(Config.LOGTAG,
1106 "connection failed with " + candidate.getHost() + ":"
1107 + candidate.getPort());
1108 connectNextCandidate();
1109 }
1110
1111 @Override
1112 public void established() {
1113 Log.d(Config.LOGTAG,
1114 "established connection with " + candidate.getHost()
1115 + ":" + candidate.getPort());
1116 sendCandidateUsed(candidate.getCid());
1117 }
1118 });
1119 }
1120
1121 private void disconnectSocks5Connections() {
1122 Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
1123 .entrySet().iterator();
1124 while (it.hasNext()) {
1125 Entry<String, JingleSocks5Transport> pairs = it.next();
1126 pairs.getValue().disconnect();
1127 it.remove();
1128 }
1129 }
1130
1131 private void sendProxyActivated(String cid) {
1132 final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
1133 final Content content = new Content(this.contentCreator, this.contentName);
1134 content.setTransportId(this.transportId);
1135 content.socks5transport().addChild("activated").setAttribute("cid", cid);
1136 packet.setJingleContent(content);
1137 this.sendJinglePacket(packet);
1138 }
1139
1140 private void sendProxyError() {
1141 final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
1142 final Content content = new Content(this.contentCreator, this.contentName);
1143 content.setTransportId(this.transportId);
1144 content.socks5transport().addChild("proxy-error");
1145 packet.setJingleContent(content);
1146 this.sendJinglePacket(packet);
1147 }
1148
1149 private void sendCandidateUsed(final String cid) {
1150 JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
1151 Content content = new Content(this.contentCreator, this.contentName);
1152 content.setTransportId(this.transportId);
1153 content.socks5transport().addChild("candidate-used").setAttribute("cid", cid);
1154 packet.setJingleContent(content);
1155 this.sentCandidate = true;
1156 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
1157 connect();
1158 }
1159 this.sendJinglePacket(packet);
1160 }
1161
1162 private void sendCandidateError() {
1163 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error");
1164 JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
1165 Content content = new Content(this.contentCreator, this.contentName);
1166 content.setTransportId(this.transportId);
1167 content.socks5transport().addChild("candidate-error");
1168 packet.setJingleContent(content);
1169 this.sentCandidate = true;
1170 this.sendJinglePacket(packet);
1171 if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) {
1172 connect();
1173 }
1174 }
1175
1176 public int getJingleStatus() {
1177 return this.mJingleStatus;
1178 }
1179
1180 private boolean equalCandidateExists(JingleCandidate candidate) {
1181 for (JingleCandidate c : this.candidates) {
1182 if (c.equalValues(candidate)) {
1183 return true;
1184 }
1185 }
1186 return false;
1187 }
1188
1189 private void mergeCandidate(JingleCandidate candidate) {
1190 for (JingleCandidate c : this.candidates) {
1191 if (c.equals(candidate)) {
1192 return;
1193 }
1194 }
1195 this.candidates.add(candidate);
1196 }
1197
1198 private void mergeCandidates(List<JingleCandidate> candidates) {
1199 Collections.sort(candidates, (a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
1200 for (JingleCandidate c : candidates) {
1201 mergeCandidate(c);
1202 }
1203 }
1204
1205 private JingleCandidate getCandidate(String cid) {
1206 for (JingleCandidate c : this.candidates) {
1207 if (c.getCid().equals(cid)) {
1208 return c;
1209 }
1210 }
1211 return null;
1212 }
1213
1214 void updateProgress(int i) {
1215 this.mProgress = i;
1216 jingleConnectionManager.updateConversationUi(false);
1217 }
1218
1219 public String getTransportId() {
1220 return this.transportId;
1221 }
1222
1223 public Content.Version getFtVersion() {
1224 return this.ftVersion;
1225 }
1226
1227 public boolean hasTransportId(String sid) {
1228 return sid.equals(this.transportId);
1229 }
1230
1231 public JingleTransport getTransport() {
1232 return this.transport;
1233 }
1234
1235 public boolean start() {
1236 if (id.account.getStatus() == Account.State.ONLINE) {
1237 if (mJingleStatus == JINGLE_STATUS_INITIATED) {
1238 new Thread(this::sendAccept).start();
1239 }
1240 return true;
1241 } else {
1242 return false;
1243 }
1244 }
1245
1246 @Override
1247 public int getStatus() {
1248 return this.mStatus;
1249 }
1250
1251 @Override
1252 public long getFileSize() {
1253 if (this.file != null) {
1254 return this.file.getExpectedSize();
1255 } else {
1256 return 0;
1257 }
1258 }
1259
1260 @Override
1261 public int getProgress() {
1262 return this.mProgress;
1263 }
1264
1265 AbstractConnectionManager getConnectionManager() {
1266 return this.jingleConnectionManager;
1267 }
1268
1269 interface OnProxyActivated {
1270 void success();
1271
1272 void failed();
1273 }
1274}