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