JingleConnection.java

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