JingleFileTransferConnection.java

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