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