JingleFileTransferConnection.java

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