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 (account.getRoster().getContact(from).showInContactList()
511 && mJingleConnectionManager.hasStoragePermission()
512 && size < this.mJingleConnectionManager.getAutoAcceptFileSize()
513 && mXmppConnectionService.isDataSaverDisabled()) {
514 Log.d(Config.LOGTAG, "auto accepting file from " + from);
515 this.acceptedAutomatically = true;
516 this.sendAccept();
517 } else {
518 message.markUnread();
519 Log.d(Config.LOGTAG,
520 "not auto accepting new file offer with size: "
521 + size
522 + " allowed size:"
523 + this.mJingleConnectionManager
524 .getAutoAcceptFileSize());
525 this.mXmppConnectionService.getNotificationService().push(message);
526 }
527 Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
528 return;
529 }
530 respondToIq(packet, false);
531 }
532 }
533
534 private static long parseLong(final Element element, final long l) {
535 final String input = element == null ? null : element.getContent();
536 if (input == null) {
537 return l;
538 }
539 try {
540 return Long.parseLong(input);
541 } catch (Exception e) {
542 return l;
543 }
544 }
545
546 private void sendInitRequest() {
547 JinglePacket packet = this.bootstrapPacket("session-initiate");
548 Content content = new Content(this.contentCreator, this.contentName);
549 if (message.isFileOrImage()) {
550 content.setTransportId(this.transportId);
551 this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
552 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
553 this.file.setKey(mXmppAxolotlMessage.getInnerKey());
554 this.file.setIv(mXmppAxolotlMessage.getIV());
555 //legacy OMEMO encrypted file transfer reported file size of the encrypted file
556 //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag)
557 this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16));
558 final Element file = content.setFileOffer(this.file, false, this.ftVersion);
559 if (remoteSupportsOmemoJet) {
560 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": remote announced support for JET");
561 final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
562 security.setAttribute("name", this.contentName);
563 security.setAttribute("cipher", JET_OMEMO_CIPHER);
564 security.setAttribute("type", AxolotlService.PEP_PREFIX);
565 security.addChild(mXmppAxolotlMessage.toElement());
566 content.addChild(security);
567 } else {
568 file.addChild(mXmppAxolotlMessage.toElement());
569 }
570 } else {
571 this.file.setExpectedSize(file.getSize());
572 content.setFileOffer(this.file, false, this.ftVersion);
573 }
574 message.resetFileParams();
575 try {
576 this.mFileInputStream = new FileInputStream(file);
577 } catch (FileNotFoundException e) {
578 fail(e.getMessage());
579 return;
580 }
581 content.setTransportId(this.transportId);
582 if (this.initialTransport == Transport.IBB) {
583 content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize));
584 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending IBB offer");
585 } else {
586 final List<Element> candidates = getCandidatesAsElements();
587 Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", account.getJid().asBareJid(), candidates.size()));
588 content.socks5transport().setChildren(candidates);
589 }
590 packet.setContent(content);
591 this.sendJinglePacket(packet, (account, response) -> {
592 if (response.getType() == IqPacket.TYPE.RESULT) {
593 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": other party received offer");
594 if (mJingleStatus == JINGLE_STATUS_OFFERED) {
595 mJingleStatus = JINGLE_STATUS_INITIATED;
596 mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
597 } else {
598 Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus);
599 }
600 } else {
601 fail(IqParser.extractErrorMessage(response));
602 }
603 });
604
605 }
606 }
607
608 private void sendHash() {
609 JinglePacket packet = this.bootstrapPacket("session-info");
610 packet.addChecksum(file.getSha1Sum(), ftVersion.getNamespace());
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.mJingleConnectionManager.updateConversationUi(true);
628 if (initialTransport == Transport.SOCKS) {
629 sendAcceptSocks();
630 } else {
631 sendAcceptIbb();
632 }
633 }
634
635 private void sendAcceptSocks() {
636 gatherAndConnectDirectCandidates();
637 this.mJingleConnectionManager.getPrimaryCandidate(this.account, initiating(), (success, candidate) -> {
638 final JinglePacket packet = bootstrapPacket("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.setContent(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.setContent(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.setContent(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("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.setContent(content);
684 this.transport.receive(file, onFileTransmissionStatusChanged);
685 this.sendJinglePacket(packet);
686 }
687
688 private JinglePacket bootstrapPacket(String action) {
689 JinglePacket packet = new JinglePacket();
690 packet.setAction(action);
691 packet.setFrom(account.getJid());
692 packet.setTo(this.message.getCounterpart());
693 packet.setSessionId(this.sessionId);
694 packet.setInitiator(this.initiator);
695 return packet;
696 }
697
698 private void sendJinglePacket(JinglePacket packet) {
699 mXmppConnectionService.sendIqPacket(account, packet, responseListener);
700 }
701
702 private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
703 mXmppConnectionService.sendIqPacket(account, packet, callback);
704 }
705
706 private void receiveAccept(JinglePacket packet) {
707 if (responding()) {
708 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order session-accept (we were responding)");
709 respondToIqWithOutOfOrder(packet);
710 return;
711 }
712 if (this.mJingleStatus != JINGLE_STATUS_INITIATED) {
713 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order session-accept");
714 respondToIqWithOutOfOrder(packet);
715 return;
716 }
717 this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
718 mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
719 Content content = packet.getJingleContent();
720 if (content.hasSocks5Transport()) {
721 respondToIq(packet, true);
722 mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
723 this.connectNextCandidate();
724 } else if (content.hasIbbTransport()) {
725 String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
726 if (receivedBlockSize != null) {
727 try {
728 int bs = Integer.parseInt(receivedBlockSize);
729 if (bs > this.ibbBlockSize) {
730 this.ibbBlockSize = bs;
731 }
732 } catch (Exception e) {
733 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in session-accept");
734 }
735 }
736 respondToIq(packet, true);
737 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
738 this.transport.connect(onIbbTransportConnected);
739 } else {
740 respondToIq(packet, false);
741 }
742 }
743
744 private void receiveTransportInfo(JinglePacket packet) {
745 final Content content = packet.getJingleContent();
746 if (content.hasSocks5Transport()) {
747 if (content.socks5transport().hasChild("activated")) {
748 respondToIq(packet, true);
749 if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
750 onProxyActivated.success();
751 } else {
752 String cid = content.socks5transport().findChild("activated").getAttribute("cid");
753 Log.d(Config.LOGTAG, "received proxy activated (" + cid
754 + ")prior to choosing our own transport");
755 JingleSocks5Transport connection = this.connections.get(cid);
756 if (connection != null) {
757 connection.setActivated(true);
758 } else {
759 Log.d(Config.LOGTAG, "activated connection not found");
760 sendSessionTerminate("failed-transport");
761 this.fail();
762 }
763 }
764 } else if (content.socks5transport().hasChild("proxy-error")) {
765 respondToIq(packet, true);
766 onProxyActivated.failed();
767 } else if (content.socks5transport().hasChild("candidate-error")) {
768 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received candidate error");
769 respondToIq(packet, true);
770 this.receivedCandidate = true;
771 if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
772 this.connect();
773 }
774 } else if (content.socks5transport().hasChild("candidate-used")) {
775 String cid = content.socks5transport().findChild("candidate-used").getAttribute("cid");
776 if (cid != null) {
777 Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
778 JingleCandidate candidate = getCandidate(cid);
779 if (candidate == null) {
780 Log.d(Config.LOGTAG, "could not find candidate with cid=" + cid);
781 respondToIq(packet, false);
782 return;
783 }
784 respondToIq(packet, true);
785 candidate.flagAsUsedByCounterpart();
786 this.receivedCandidate = true;
787 if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
788 this.connect();
789 } else {
790 Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status=" + mJingleStatus + " sentCandidate=" + sentCandidate);
791 }
792 } else {
793 respondToIq(packet, false);
794 }
795 } else {
796 respondToIq(packet, false);
797 }
798 } else {
799 respondToIq(packet, true);
800 }
801 }
802
803 private void connect() {
804 final JingleSocks5Transport connection = chooseConnection();
805 this.transport = connection;
806 if (connection == null) {
807 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not find suitable candidate");
808 this.disconnectSocks5Connections();
809 if (initiating()) {
810 this.sendFallbackToIbb();
811 }
812 } else {
813 final JingleCandidate candidate = connection.getCandidate();
814 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": elected candidate " + candidate.getHost() + ":" + candidate.getPort());
815 this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
816 if (connection.needsActivation()) {
817 if (connection.getCandidate().isOurs()) {
818 final String sid;
819 if (ftVersion == Content.Version.FT_3) {
820 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy");
821 sid = getSessionId();
822 } else {
823 sid = getTransportId();
824 }
825 Log.d(Config.LOGTAG, "candidate "
826 + connection.getCandidate().getCid()
827 + " was our proxy. going to activate");
828 IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
829 activation.setTo(connection.getCandidate().getJid());
830 activation.query("http://jabber.org/protocol/bytestreams")
831 .setAttribute("sid", sid);
832 activation.query().addChild("activate")
833 .setContent(this.getCounterPart().toString());
834 mXmppConnectionService.sendIqPacket(account, activation, (account, response) -> {
835 if (response.getType() != IqPacket.TYPE.RESULT) {
836 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + response.toString());
837 sendProxyError();
838 onProxyActivated.failed();
839 } else {
840 sendProxyActivated(connection.getCandidate().getCid());
841 onProxyActivated.success();
842 }
843 });
844 } else {
845 Log.d(Config.LOGTAG,
846 "candidate "
847 + connection.getCandidate().getCid()
848 + " was a proxy. waiting for other party to activate");
849 }
850 } else {
851 if (initiating()) {
852 Log.d(Config.LOGTAG, "we were initiating. sending file");
853 connection.send(file, onFileTransmissionStatusChanged);
854 } else {
855 Log.d(Config.LOGTAG, "we were responding. receiving file");
856 connection.receive(file, onFileTransmissionStatusChanged);
857 }
858 }
859 }
860 }
861
862 private JingleSocks5Transport chooseConnection() {
863 JingleSocks5Transport connection = null;
864 for (Entry<String, JingleSocks5Transport> cursor : connections
865 .entrySet()) {
866 JingleSocks5Transport currentConnection = cursor.getValue();
867 // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
868 if (currentConnection.isEstablished()
869 && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
870 .getCandidate().isOurs()))) {
871 // Log.d(Config.LOGTAG,"is usable");
872 if (connection == null) {
873 connection = currentConnection;
874 } else {
875 if (connection.getCandidate().getPriority() < currentConnection
876 .getCandidate().getPriority()) {
877 connection = currentConnection;
878 } else if (connection.getCandidate().getPriority() == currentConnection
879 .getCandidate().getPriority()) {
880 // Log.d(Config.LOGTAG,"found two candidates with same priority");
881 if (initiating()) {
882 if (currentConnection.getCandidate().isOurs()) {
883 connection = currentConnection;
884 }
885 } else {
886 if (!currentConnection.getCandidate().isOurs()) {
887 connection = currentConnection;
888 }
889 }
890 }
891 }
892 }
893 }
894 return connection;
895 }
896
897 private void sendSuccess() {
898 sendSessionTerminate("success");
899 this.disconnectSocks5Connections();
900 this.mJingleStatus = JINGLE_STATUS_FINISHED;
901 this.message.setStatus(Message.STATUS_RECEIVED);
902 this.message.setTransferable(null);
903 this.mXmppConnectionService.updateMessage(message, false);
904 this.mJingleConnectionManager.finishConnection(this);
905 }
906
907 private void sendFallbackToIbb() {
908 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending fallback to ibb");
909 JinglePacket packet = this.bootstrapPacket("transport-replace");
910 Content content = new Content(this.contentCreator, this.contentName);
911 this.transportId = this.mJingleConnectionManager.nextRandomId();
912 content.setTransportId(this.transportId);
913 content.ibbTransport().setAttribute("block-size",
914 Integer.toString(this.ibbBlockSize));
915 packet.setContent(content);
916 this.sendJinglePacket(packet);
917 }
918
919
920 private void receiveFallbackToIbb(JinglePacket packet) {
921 if (initiating()) {
922 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order transport-replace (we were initiating)");
923 respondToIqWithOutOfOrder(packet);
924 return;
925 }
926 final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING);
927 if (!validState) {
928 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order transport-replace");
929 respondToIqWithOutOfOrder(packet);
930 return;
931 }
932 this.proxyActivationFailed = false; //fallback received; now we no longer need to accept another one;
933 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": receiving fallback to ibb");
934 final String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
935 if (receivedBlockSize != null) {
936 try {
937 final int bs = Integer.parseInt(receivedBlockSize);
938 if (bs < this.ibbBlockSize) {
939 this.ibbBlockSize = bs;
940 }
941 } catch (NumberFormatException e) {
942 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in transport-replace");
943 }
944 }
945 this.transportId = packet.getJingleContent().getTransportId();
946 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
947
948 final JinglePacket answer = bootstrapPacket("transport-accept");
949
950 final Content content = new Content(contentCreator, contentName);
951 content.ibbTransport().setAttribute("block-size", this.ibbBlockSize);
952 content.ibbTransport().setAttribute("sid", this.transportId);
953 answer.setContent(content);
954
955 respondToIq(packet, true);
956
957 if (initiating()) {
958 this.sendJinglePacket(answer, (account, response) -> {
959 if (response.getType() == IqPacket.TYPE.RESULT) {
960 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb");
961 transport.connect(onIbbTransportConnected);
962 }
963 });
964 } else {
965 this.transport.receive(file, onFileTransmissionStatusChanged);
966 this.sendJinglePacket(answer);
967 }
968 }
969
970 private void receiveTransportAccept(JinglePacket packet) {
971 if (responding()) {
972 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order transport-accept (we were responding)");
973 respondToIqWithOutOfOrder(packet);
974 return;
975 }
976 final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING);
977 if (!validState) {
978 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order transport-accept");
979 respondToIqWithOutOfOrder(packet);
980 return;
981 }
982 this.proxyActivationFailed = false; //fallback accepted; now we no longer need to accept another one;
983 if (packet.getJingleContent().hasIbbTransport()) {
984 final Element ibbTransport = packet.getJingleContent().ibbTransport();
985 final String receivedBlockSize = ibbTransport.getAttribute("block-size");
986 final String sid = ibbTransport.getAttribute("sid");
987 if (receivedBlockSize != null) {
988 try {
989 int bs = Integer.parseInt(receivedBlockSize);
990 if (bs < this.ibbBlockSize) {
991 this.ibbBlockSize = bs;
992 }
993 } catch (NumberFormatException e) {
994 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in transport-accept");
995 }
996 }
997 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
998
999 if (sid == null || !sid.equals(this.transportId)) {
1000 Log.w(Config.LOGTAG, String.format("%s: sid in transport-accept (%s) did not match our sid (%s) ", account.getJid().asBareJid(), sid, transportId));
1001 }
1002 respondToIq(packet, true);
1003 //might be receive instead if we are not initiating
1004 if (initiating()) {
1005 this.transport.connect(onIbbTransportConnected);
1006 }
1007 } else {
1008 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invalid transport-accept");
1009 respondToIq(packet, false);
1010 }
1011 }
1012
1013 private void receiveSuccess() {
1014 if (initiating()) {
1015 this.mJingleStatus = JINGLE_STATUS_FINISHED;
1016 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED);
1017 this.disconnectSocks5Connections();
1018 if (this.transport instanceof JingleInbandTransport) {
1019 this.transport.disconnect();
1020 }
1021 this.message.setTransferable(null);
1022 this.mJingleConnectionManager.finishConnection(this);
1023 } else {
1024 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received session-terminate/success while responding");
1025 }
1026 }
1027
1028 @Override
1029 public void cancel() {
1030 this.cancelled = true;
1031 abort("cancel");
1032 }
1033
1034 void abort(final String reason) {
1035 this.disconnectSocks5Connections();
1036 if (this.transport instanceof JingleInbandTransport) {
1037 this.transport.disconnect();
1038 }
1039 sendSessionTerminate(reason);
1040 this.mJingleConnectionManager.finishConnection(this);
1041 if (responding()) {
1042 this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
1043 if (this.file != null) {
1044 file.delete();
1045 }
1046 this.mJingleConnectionManager.updateConversationUi(true);
1047 } else {
1048 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null);
1049 this.message.setTransferable(null);
1050 }
1051 }
1052
1053 private void fail() {
1054 fail(null);
1055 }
1056
1057 private void fail(String errorMessage) {
1058 this.mJingleStatus = JINGLE_STATUS_FAILED;
1059 this.disconnectSocks5Connections();
1060 if (this.transport instanceof JingleInbandTransport) {
1061 this.transport.disconnect();
1062 }
1063 FileBackend.close(mFileInputStream);
1064 FileBackend.close(mFileOutputStream);
1065 if (this.message != null) {
1066 if (responding()) {
1067 this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
1068 if (this.file != null) {
1069 file.delete();
1070 }
1071 this.mJingleConnectionManager.updateConversationUi(true);
1072 } else {
1073 this.mXmppConnectionService.markMessage(this.message,
1074 Message.STATUS_SEND_FAILED,
1075 cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
1076 this.message.setTransferable(null);
1077 }
1078 }
1079 this.mJingleConnectionManager.finishConnection(this);
1080 }
1081
1082 private void sendSessionTerminate(String reason) {
1083 final JinglePacket packet = bootstrapPacket("session-terminate");
1084 final Reason r = new Reason();
1085 r.addChild(reason);
1086 packet.setReason(r);
1087 this.sendJinglePacket(packet);
1088 }
1089
1090 private void connectNextCandidate() {
1091 for (JingleCandidate candidate : this.candidates) {
1092 if ((!connections.containsKey(candidate.getCid()) && (!candidate
1093 .isOurs()))) {
1094 this.connectWithCandidate(candidate);
1095 return;
1096 }
1097 }
1098 this.sendCandidateError();
1099 }
1100
1101 private void connectWithCandidate(final JingleCandidate candidate) {
1102 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
1103 this, candidate);
1104 connections.put(candidate.getCid(), socksConnection);
1105 socksConnection.connect(new OnTransportConnected() {
1106
1107 @Override
1108 public void failed() {
1109 Log.d(Config.LOGTAG,
1110 "connection failed with " + candidate.getHost() + ":"
1111 + candidate.getPort());
1112 connectNextCandidate();
1113 }
1114
1115 @Override
1116 public void established() {
1117 Log.d(Config.LOGTAG,
1118 "established connection with " + candidate.getHost()
1119 + ":" + candidate.getPort());
1120 sendCandidateUsed(candidate.getCid());
1121 }
1122 });
1123 }
1124
1125 private void disconnectSocks5Connections() {
1126 Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
1127 .entrySet().iterator();
1128 while (it.hasNext()) {
1129 Entry<String, JingleSocks5Transport> pairs = it.next();
1130 pairs.getValue().disconnect();
1131 it.remove();
1132 }
1133 }
1134
1135 private void sendProxyActivated(String cid) {
1136 final JinglePacket packet = bootstrapPacket("transport-info");
1137 final Content content = new Content(this.contentCreator, this.contentName);
1138 content.setTransportId(this.transportId);
1139 content.socks5transport().addChild("activated").setAttribute("cid", cid);
1140 packet.setContent(content);
1141 this.sendJinglePacket(packet);
1142 }
1143
1144 private void sendProxyError() {
1145 final JinglePacket packet = bootstrapPacket("transport-info");
1146 final Content content = new Content(this.contentCreator, this.contentName);
1147 content.setTransportId(this.transportId);
1148 content.socks5transport().addChild("proxy-error");
1149 packet.setContent(content);
1150 this.sendJinglePacket(packet);
1151 }
1152
1153 private void sendCandidateUsed(final String cid) {
1154 JinglePacket packet = bootstrapPacket("transport-info");
1155 Content content = new Content(this.contentCreator, this.contentName);
1156 content.setTransportId(this.transportId);
1157 content.socks5transport().addChild("candidate-used").setAttribute("cid", cid);
1158 packet.setContent(content);
1159 this.sentCandidate = true;
1160 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
1161 connect();
1162 }
1163 this.sendJinglePacket(packet);
1164 }
1165
1166 private void sendCandidateError() {
1167 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending candidate error");
1168 JinglePacket packet = bootstrapPacket("transport-info");
1169 Content content = new Content(this.contentCreator, this.contentName);
1170 content.setTransportId(this.transportId);
1171 content.socks5transport().addChild("candidate-error");
1172 packet.setContent(content);
1173 this.sentCandidate = true;
1174 this.sendJinglePacket(packet);
1175 if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) {
1176 connect();
1177 }
1178 }
1179
1180 public int getJingleStatus() {
1181 return this.mJingleStatus;
1182 }
1183
1184 private boolean equalCandidateExists(JingleCandidate candidate) {
1185 for (JingleCandidate c : this.candidates) {
1186 if (c.equalValues(candidate)) {
1187 return true;
1188 }
1189 }
1190 return false;
1191 }
1192
1193 private void mergeCandidate(JingleCandidate candidate) {
1194 for (JingleCandidate c : this.candidates) {
1195 if (c.equals(candidate)) {
1196 return;
1197 }
1198 }
1199 this.candidates.add(candidate);
1200 }
1201
1202 private void mergeCandidates(List<JingleCandidate> candidates) {
1203 Collections.sort(candidates, (a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
1204 for (JingleCandidate c : candidates) {
1205 mergeCandidate(c);
1206 }
1207 }
1208
1209 private JingleCandidate getCandidate(String cid) {
1210 for (JingleCandidate c : this.candidates) {
1211 if (c.getCid().equals(cid)) {
1212 return c;
1213 }
1214 }
1215 return null;
1216 }
1217
1218 void updateProgress(int i) {
1219 this.mProgress = i;
1220 mJingleConnectionManager.updateConversationUi(false);
1221 }
1222
1223 public String getTransportId() {
1224 return this.transportId;
1225 }
1226
1227 public Content.Version getFtVersion() {
1228 return this.ftVersion;
1229 }
1230
1231 public boolean hasTransportId(String sid) {
1232 return sid.equals(this.transportId);
1233 }
1234
1235 public JingleTransport getTransport() {
1236 return this.transport;
1237 }
1238
1239 public boolean start() {
1240 if (account.getStatus() == Account.State.ONLINE) {
1241 if (mJingleStatus == JINGLE_STATUS_INITIATED) {
1242 new Thread(this::sendAccept).start();
1243 }
1244 return true;
1245 } else {
1246 return false;
1247 }
1248 }
1249
1250 @Override
1251 public int getStatus() {
1252 return this.mStatus;
1253 }
1254
1255 @Override
1256 public long getFileSize() {
1257 if (this.file != null) {
1258 return this.file.getExpectedSize();
1259 } else {
1260 return 0;
1261 }
1262 }
1263
1264 @Override
1265 public int getProgress() {
1266 return this.mProgress;
1267 }
1268
1269 public AbstractConnectionManager getConnectionManager() {
1270 return this.mJingleConnectionManager;
1271 }
1272
1273 interface OnProxyActivated {
1274 void success();
1275
1276 void failed();
1277 }
1278}