XmppConnection.java

   1package eu.siacs.conversations.xmpp;
   2
   3import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
   4
   5import android.content.Context;
   6import android.graphics.Bitmap;
   7import android.graphics.BitmapFactory;
   8import android.os.Build;
   9import android.os.SystemClock;
  10import android.security.KeyChain;
  11import android.util.Base64;
  12import android.util.Log;
  13import android.util.Pair;
  14import android.util.SparseArray;
  15
  16import androidx.annotation.NonNull;
  17import androidx.annotation.Nullable;
  18
  19import com.google.common.base.MoreObjects;
  20import com.google.common.base.Optional;
  21import com.google.common.base.Preconditions;
  22import com.google.common.base.Strings;
  23import com.google.common.collect.ImmutableList;
  24
  25import eu.siacs.conversations.Config;
  26import eu.siacs.conversations.R;
  27import eu.siacs.conversations.crypto.XmppDomainVerifier;
  28import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  29import eu.siacs.conversations.crypto.sasl.ChannelBinding;
  30import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism;
  31import eu.siacs.conversations.crypto.sasl.HashedToken;
  32import eu.siacs.conversations.crypto.sasl.SaslMechanism;
  33import eu.siacs.conversations.entities.Account;
  34import eu.siacs.conversations.entities.Message;
  35import eu.siacs.conversations.entities.ServiceDiscoveryResult;
  36import eu.siacs.conversations.generator.IqGenerator;
  37import eu.siacs.conversations.http.HttpConnectionManager;
  38import eu.siacs.conversations.persistance.FileBackend;
  39import eu.siacs.conversations.services.MemorizingTrustManager;
  40import eu.siacs.conversations.services.MessageArchiveService;
  41import eu.siacs.conversations.services.NotificationService;
  42import eu.siacs.conversations.services.XmppConnectionService;
  43import eu.siacs.conversations.utils.AccountUtils;
  44import eu.siacs.conversations.utils.CryptoHelper;
  45import eu.siacs.conversations.utils.Patterns;
  46import eu.siacs.conversations.utils.PhoneHelper;
  47import eu.siacs.conversations.utils.Resolver;
  48import eu.siacs.conversations.utils.SSLSockets;
  49import eu.siacs.conversations.utils.SocksSocketFactory;
  50import eu.siacs.conversations.utils.XmlHelper;
  51import eu.siacs.conversations.xml.Element;
  52import eu.siacs.conversations.xml.LocalizedContent;
  53import eu.siacs.conversations.xml.Namespace;
  54import eu.siacs.conversations.xml.Tag;
  55import eu.siacs.conversations.xml.TagWriter;
  56import eu.siacs.conversations.xml.XmlReader;
  57import eu.siacs.conversations.xmpp.bind.Bind2;
  58import eu.siacs.conversations.xmpp.forms.Data;
  59import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  60import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  61import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza;
  62import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
  63import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  64import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  65import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  66import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
  67import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
  68import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
  69import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
  70import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
  71import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
  72
  73import okhttp3.HttpUrl;
  74
  75import org.xmlpull.v1.XmlPullParserException;
  76
  77import java.io.ByteArrayInputStream;
  78import java.io.IOException;
  79import java.io.InputStream;
  80import java.net.ConnectException;
  81import java.net.IDN;
  82import java.net.InetAddress;
  83import java.net.InetSocketAddress;
  84import java.net.Socket;
  85import java.net.UnknownHostException;
  86import java.security.KeyManagementException;
  87import java.security.NoSuchAlgorithmException;
  88import java.security.Principal;
  89import java.security.PrivateKey;
  90import java.security.cert.X509Certificate;
  91import java.util.ArrayList;
  92import java.util.Arrays;
  93import java.util.Collection;
  94import java.util.Collections;
  95import java.util.HashMap;
  96import java.util.HashSet;
  97import java.util.Hashtable;
  98import java.util.Iterator;
  99import java.util.List;
 100import java.util.Map.Entry;
 101import java.util.Set;
 102import java.util.concurrent.CountDownLatch;
 103import java.util.concurrent.TimeUnit;
 104import java.util.concurrent.atomic.AtomicBoolean;
 105import java.util.concurrent.atomic.AtomicInteger;
 106import java.util.regex.Matcher;
 107
 108import javax.net.ssl.KeyManager;
 109import javax.net.ssl.SSLContext;
 110import javax.net.ssl.SSLPeerUnverifiedException;
 111import javax.net.ssl.SSLSocket;
 112import javax.net.ssl.SSLSocketFactory;
 113import javax.net.ssl.X509KeyManager;
 114import javax.net.ssl.X509TrustManager;
 115
 116public class XmppConnection implements Runnable {
 117
 118    private static final int PACKET_IQ = 0;
 119    private static final int PACKET_MESSAGE = 1;
 120    private static final int PACKET_PRESENCE = 2;
 121    public final OnIqPacketReceived registrationResponseListener =
 122            (account, packet) -> {
 123                if (packet.getType() == IqPacket.TYPE.RESULT) {
 124                    account.setOption(Account.OPTION_REGISTER, false);
 125                    Log.d(
 126                            Config.LOGTAG,
 127                            account.getJid().asBareJid()
 128                                    + ": successfully registered new account on server");
 129                    throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
 130                } else {
 131                    final List<String> PASSWORD_TOO_WEAK_MSGS =
 132                            Arrays.asList(
 133                                    "The password is too weak", "Please use a longer password.");
 134                    Element error = packet.findChild("error");
 135                    Account.State state = Account.State.REGISTRATION_FAILED;
 136                    if (error != null) {
 137                        if (error.hasChild("conflict")) {
 138                            state = Account.State.REGISTRATION_CONFLICT;
 139                        } else if (error.hasChild("resource-constraint")
 140                                && "wait".equals(error.getAttribute("type"))) {
 141                            state = Account.State.REGISTRATION_PLEASE_WAIT;
 142                        } else if (error.hasChild("not-acceptable")
 143                                && PASSWORD_TOO_WEAK_MSGS.contains(
 144                                        error.findChildContent("text"))) {
 145                            state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
 146                        }
 147                    }
 148                    throw new StateChangingError(state);
 149                }
 150            };
 151    protected final Account account;
 152    private final Features features = new Features(this);
 153    private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
 154    private final HashMap<String, Jid> commands = new HashMap<>();
 155    private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
 156    private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks =
 157            new Hashtable<>();
 158    private final Set<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners =
 159            new HashSet<>();
 160    private final XmppConnectionService mXmppConnectionService;
 161    private Socket socket;
 162    private XmlReader tagReader;
 163    private TagWriter tagWriter = new TagWriter();
 164    private boolean shouldAuthenticate = true;
 165    private boolean inSmacksSession = false;
 166    private boolean quickStartInProgress = false;
 167    private boolean isBound = false;
 168    private Element streamFeatures;
 169    private Element boundStreamFeatures;
 170    private StreamId streamId = null;
 171    private int stanzasReceived = 0;
 172    private int stanzasSent = 0;
 173    private int stanzasSentBeforeAuthentication;
 174    private long lastPacketReceived = 0;
 175    private long lastPingSent = 0;
 176    private long lastConnect = 0;
 177    private long lastSessionStarted = 0;
 178    private long lastDiscoStarted = 0;
 179    private boolean isMamPreferenceAlways = false;
 180    private final AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
 181    private final AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
 182    private final AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
 183    private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
 184    private boolean mInteractive = false;
 185    private int attempt = 0;
 186    private OnPresencePacketReceived presenceListener = null;
 187    private OnJinglePacketReceived jingleListener = null;
 188    private OnIqPacketReceived unregisteredIqListener = null;
 189    private OnMessagePacketReceived messageListener = null;
 190    private OnStatusChanged statusListener = null;
 191    private OnBindListener bindListener = null;
 192    private OnMessageAcknowledged acknowledgedListener = null;
 193    private LoginInfo loginInfo;
 194    private HashedToken.Mechanism hashTokenRequest;
 195    private HttpUrl redirectionUrl = null;
 196    private String verifiedHostname = null;
 197    private Resolver.Result currentResolverResult;
 198    private Resolver.Result seeOtherHostResolverResult;
 199    private volatile Thread mThread;
 200    private CountDownLatch mStreamCountDownLatch;
 201
 202    public XmppConnection(final Account account, final XmppConnectionService service) {
 203        this.account = account;
 204        this.mXmppConnectionService = service;
 205    }
 206
 207    private static void fixResource(Context context, Account account) {
 208        String resource = account.getResource();
 209        int fixedPartLength =
 210                context.getString(R.string.app_name).length() + 1; // include the trailing dot
 211        int randomPartLength = 4; // 3 bytes
 212        if (resource != null && resource.length() > fixedPartLength + randomPartLength) {
 213            if (validBase64(
 214                    resource.substring(fixedPartLength, fixedPartLength + randomPartLength))) {
 215                account.setResource(resource.substring(0, fixedPartLength + randomPartLength));
 216            }
 217        }
 218    }
 219
 220    private static boolean validBase64(String input) {
 221        try {
 222            return Base64.decode(input, Base64.URL_SAFE).length == 3;
 223        } catch (Throwable throwable) {
 224            return false;
 225        }
 226    }
 227
 228    private void changeStatus(final Account.State nextStatus) {
 229        synchronized (this) {
 230            if (Thread.currentThread().isInterrupted()) {
 231                Log.d(
 232                        Config.LOGTAG,
 233                        account.getJid().asBareJid()
 234                                + ": not changing status to "
 235                                + nextStatus
 236                                + " because thread was interrupted");
 237                return;
 238            }
 239            if (account.getStatus() != nextStatus) {
 240                if (nextStatus == Account.State.OFFLINE
 241                        && account.getStatus() != Account.State.CONNECTING
 242                        && account.getStatus() != Account.State.ONLINE
 243                        && account.getStatus() != Account.State.DISABLED
 244                        && account.getStatus() != Account.State.LOGGED_OUT) {
 245                    return;
 246                }
 247                if (nextStatus == Account.State.ONLINE) {
 248                    this.attempt = 0;
 249                }
 250                account.setStatus(nextStatus);
 251            } else {
 252                return;
 253            }
 254        }
 255        if (statusListener != null) {
 256            statusListener.onStatusChanged(account);
 257        }
 258    }
 259
 260    public Jid getJidForCommand(final String node) {
 261        synchronized (this.commands) {
 262            return this.commands.get(node);
 263        }
 264    }
 265
 266    public void prepareNewConnection() {
 267        this.lastConnect = SystemClock.elapsedRealtime();
 268        this.lastPingSent = SystemClock.elapsedRealtime();
 269        this.lastDiscoStarted = Long.MAX_VALUE;
 270        this.mWaitingForSmCatchup.set(false);
 271        this.changeStatus(Account.State.CONNECTING);
 272    }
 273
 274    public boolean isWaitingForSmCatchup() {
 275        return mWaitingForSmCatchup.get();
 276    }
 277
 278    public void incrementSmCatchupMessageCounter() {
 279        this.mSmCatchupMessageCounter.incrementAndGet();
 280    }
 281
 282    protected void connect() {
 283        if (mXmppConnectionService.areMessagesInitialized()) {
 284            mXmppConnectionService.resetSendingToWaiting(account);
 285        }
 286        Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting");
 287        this.loginInfo = null;
 288        this.features.encryptionEnabled = false;
 289        this.inSmacksSession = false;
 290        this.quickStartInProgress = false;
 291        this.isBound = false;
 292        this.attempt++;
 293        this.verifiedHostname =
 294                null; // will be set if user entered hostname is being used or hostname was verified
 295        // with dnssec
 296        try {
 297            Socket localSocket;
 298            shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER);
 299            this.changeStatus(Account.State.CONNECTING);
 300            final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion();
 301            final boolean extended = mXmppConnectionService.showExtendedConnectionOptions();
 302            if (useTor) {
 303                String destination;
 304                if (account.getHostname().isEmpty() || account.isOnion()) {
 305                    destination = account.getServer();
 306                } else {
 307                    destination = account.getHostname();
 308                    this.verifiedHostname = destination;
 309                }
 310
 311                final int port = account.getPort();
 312                final boolean directTls = Resolver.useDirectTls(port);
 313
 314                Log.d(
 315                        Config.LOGTAG,
 316                        account.getJid().asBareJid()
 317                                + ": connect to "
 318                                + destination
 319                                + " via Tor. directTls="
 320                                + directTls);
 321                localSocket = SocksSocketFactory.createSocketOverTor(destination, port);
 322
 323                if (directTls) {
 324                    localSocket = upgradeSocketToTls(localSocket);
 325                    features.encryptionEnabled = true;
 326                }
 327
 328                try {
 329                    startXmpp(localSocket);
 330                } catch (final InterruptedException e) {
 331                    Log.d(
 332                            Config.LOGTAG,
 333                            account.getJid().asBareJid()
 334                                    + ": thread was interrupted before beginning stream");
 335                    return;
 336                } catch (final Exception e) {
 337                    throw new IOException("Could not start stream", e);
 338                }
 339            } else {
 340                final String domain = account.getServer();
 341                final List<Resolver.Result> results;
 342                final boolean hardcoded = extended && !account.getHostname().isEmpty();
 343                if (hardcoded) {
 344                    results = Resolver.fromHardCoded(account.getHostname(), account.getPort());
 345                } else {
 346                    results = Resolver.resolve(domain);
 347                }
 348                if (Thread.currentThread().isInterrupted()) {
 349                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted");
 350                    return;
 351                }
 352                if (results.size() == 0) {
 353                    Log.e(
 354                            Config.LOGTAG,
 355                            account.getJid().asBareJid() + ": Resolver results were empty");
 356                    return;
 357                }
 358                final Resolver.Result storedBackupResult;
 359                if (hardcoded) {
 360                    storedBackupResult = null;
 361                } else {
 362                    storedBackupResult =
 363                            mXmppConnectionService.databaseBackend.findResolverResult(domain);
 364                    if (storedBackupResult != null && !results.contains(storedBackupResult)) {
 365                        results.add(storedBackupResult);
 366                        Log.d(
 367                                Config.LOGTAG,
 368                                account.getJid().asBareJid()
 369                                        + ": loaded backup resolver result from db: "
 370                                        + storedBackupResult);
 371                    }
 372                }
 373                final StreamId streamId = this.streamId;
 374                final Resolver.Result resumeLocation = streamId == null ? null : streamId.location;
 375                if (resumeLocation != null) {
 376                    Log.d(
 377                            Config.LOGTAG,
 378                            account.getJid().asBareJid()
 379                                    + ": injected resume location on position 0");
 380                    results.add(0, resumeLocation);
 381                }
 382                final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult;
 383                if (seeOtherHost != null) {
 384                    Log.d(
 385                            Config.LOGTAG,
 386                            account.getJid().asBareJid()
 387                                    + ": injected see-other-host on position 0");
 388                    results.add(0, seeOtherHost);
 389                }
 390                for (final Iterator<Resolver.Result> iterator = results.iterator();
 391                        iterator.hasNext(); ) {
 392                    final Resolver.Result result = iterator.next();
 393                    if (Thread.currentThread().isInterrupted()) {
 394                        Log.d(
 395                                Config.LOGTAG,
 396                                account.getJid().asBareJid() + ": Thread was interrupted");
 397                        return;
 398                    }
 399                    try {
 400                        // if tls is true, encryption is implied and must not be started
 401                        features.encryptionEnabled = result.isDirectTls();
 402                        verifiedHostname =
 403                                result.isAuthenticated() ? result.getHostname().toString() : null;
 404                        final InetSocketAddress addr;
 405                        if (result.getIp() != null) {
 406                            addr = new InetSocketAddress(result.getIp(), result.getPort());
 407                            Log.d(
 408                                    Config.LOGTAG,
 409                                    account.getJid().asBareJid().toString()
 410                                            + ": using values from resolver "
 411                                            + (result.getHostname() == null
 412                                                    ? ""
 413                                                    : result.getHostname().toString() + "/")
 414                                            + result.getIp().getHostAddress()
 415                                            + ":"
 416                                            + result.getPort()
 417                                            + " tls: "
 418                                            + features.encryptionEnabled);
 419                        } else {
 420                            addr =
 421                                    new InetSocketAddress(
 422                                            IDN.toASCII(result.getHostname().toString()),
 423                                            result.getPort());
 424                            Log.d(
 425                                    Config.LOGTAG,
 426                                    account.getJid().asBareJid().toString()
 427                                            + ": using values from resolver "
 428                                            + result.getHostname().toString()
 429                                            + ":"
 430                                            + result.getPort()
 431                                            + " tls: "
 432                                            + features.encryptionEnabled);
 433                        }
 434
 435                        localSocket = new Socket();
 436                        localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
 437
 438                        if (features.encryptionEnabled) {
 439                            localSocket = upgradeSocketToTls(localSocket);
 440                        }
 441
 442                        localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000);
 443                        if (startXmpp(localSocket)) {
 444                            localSocket.setSoTimeout(
 445                                    0); // reset to 0; once the connection is established we don’t
 446                            // want this
 447                            if (!hardcoded && !result.equals(storedBackupResult)) {
 448                                mXmppConnectionService.databaseBackend.saveResolverResult(
 449                                        domain, result);
 450                            }
 451                            this.currentResolverResult = result;
 452                            this.seeOtherHostResolverResult = null;
 453                            break; // successfully connected to server that speaks xmpp
 454                        } else {
 455                            FileBackend.close(localSocket);
 456                            throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
 457                        }
 458                    } catch (final StateChangingException e) {
 459                        if (!iterator.hasNext()) {
 460                            throw e;
 461                        }
 462                    } catch (InterruptedException e) {
 463                        Log.d(
 464                                Config.LOGTAG,
 465                                account.getJid().asBareJid()
 466                                        + ": thread was interrupted before beginning stream");
 467                        return;
 468                    } catch (final Throwable e) {
 469                        Log.d(
 470                                Config.LOGTAG,
 471                                account.getJid().asBareJid().toString()
 472                                        + ": "
 473                                        + e.getMessage()
 474                                        + "("
 475                                        + e.getClass().getName()
 476                                        + ")");
 477                        if (!iterator.hasNext()) {
 478                            throw new UnknownHostException();
 479                        }
 480                    }
 481                }
 482            }
 483            processStream();
 484        } catch (final SecurityException e) {
 485            this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
 486        } catch (final StateChangingException e) {
 487            this.changeStatus(e.state);
 488        } catch (final UnknownHostException
 489                | ConnectException
 490                | SocksSocketFactory.HostNotFoundException e) {
 491            this.changeStatus(Account.State.SERVER_NOT_FOUND);
 492        } catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
 493            this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
 494        } catch (final IOException | XmlPullParserException e) {
 495            Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage());
 496            this.changeStatus(Account.State.OFFLINE);
 497            this.attempt = Math.max(0, this.attempt - 1);
 498        } finally {
 499            if (!Thread.currentThread().isInterrupted()) {
 500                forceCloseSocket();
 501            } else {
 502                Log.d(
 503                        Config.LOGTAG,
 504                        account.getJid().asBareJid()
 505                                + ": not force closing socket because thread was interrupted");
 506            }
 507        }
 508    }
 509
 510    /**
 511     * Starts xmpp protocol, call after connecting to socket
 512     *
 513     * @return true if server returns with valid xmpp, false otherwise
 514     */
 515    private boolean startXmpp(final Socket socket) throws Exception {
 516        if (Thread.currentThread().isInterrupted()) {
 517            throw new InterruptedException();
 518        }
 519        this.socket = socket;
 520        tagReader = new XmlReader();
 521        if (tagWriter != null) {
 522            tagWriter.forceClose();
 523        }
 524        tagWriter = new TagWriter();
 525        tagWriter.setOutputStream(socket.getOutputStream());
 526        tagReader.setInputStream(socket.getInputStream());
 527        tagWriter.beginDocument();
 528        final boolean quickStart;
 529        if (socket instanceof SSLSocket sslSocket) {
 530            SSLSockets.log(account, sslSocket);
 531            quickStart = establishStream(SSLSockets.version(sslSocket));
 532        } else {
 533            quickStart = establishStream(SSLSockets.Version.NONE);
 534        }
 535        final Tag tag = tagReader.readTag();
 536        if (Thread.currentThread().isInterrupted()) {
 537            throw new InterruptedException();
 538        }
 539        if (tag == null) {
 540            return false;
 541        }
 542        final boolean success = tag.isStart("stream", Namespace.STREAMS);
 543        if (success) {
 544            final var from = tag.getAttribute("from");
 545            if (from == null || !from.equals(account.getServer())) {
 546                throw new StateChangingException(Account.State.HOST_UNKNOWN);
 547            }
 548        }
 549        if (success && quickStart) {
 550            this.quickStartInProgress = true;
 551        }
 552        return success;
 553    }
 554
 555    private SSLSocketFactory getSSLSocketFactory()
 556            throws NoSuchAlgorithmException, KeyManagementException {
 557        final SSLContext sc = SSLSockets.getSSLContext();
 558        final MemorizingTrustManager trustManager =
 559                this.mXmppConnectionService.getMemorizingTrustManager();
 560        final KeyManager[] keyManager;
 561        if (account.getPrivateKeyAlias() != null) {
 562            keyManager = new KeyManager[] {new MyKeyManager()};
 563        } else {
 564            keyManager = null;
 565        }
 566        final String domain = account.getServer();
 567        sc.init(
 568                keyManager,
 569                new X509TrustManager[] {
 570                    mInteractive
 571                            ? trustManager.getInteractive(domain)
 572                            : trustManager.getNonInteractive(domain)
 573                },
 574                SECURE_RANDOM);
 575        return sc.getSocketFactory();
 576    }
 577
 578    @Override
 579    public void run() {
 580        synchronized (this) {
 581            this.mThread = Thread.currentThread();
 582            if (this.mThread.isInterrupted()) {
 583                Log.d(
 584                        Config.LOGTAG,
 585                        account.getJid().asBareJid()
 586                                + ": aborting connect because thread was interrupted");
 587                return;
 588            }
 589            forceCloseSocket();
 590        }
 591        connect();
 592    }
 593
 594    private void processStream() throws XmlPullParserException, IOException {
 595        final CountDownLatch streamCountDownLatch = new CountDownLatch(1);
 596        this.mStreamCountDownLatch = streamCountDownLatch;
 597        Tag nextTag = tagReader.readTag();
 598        while (nextTag != null && !nextTag.isEnd("stream")) {
 599            if (nextTag.isStart("error")) {
 600                processStreamError(nextTag);
 601            } else if (nextTag.isStart("features", Namespace.STREAMS)) {
 602                processStreamFeatures(nextTag);
 603            } else if (nextTag.isStart("proceed", Namespace.TLS)) {
 604                switchOverToTls();
 605            } else if (nextTag.isStart("failure", Namespace.TLS)) {
 606                throw new StateChangingException(Account.State.TLS_ERROR);
 607            } else if (account.isOptionSet(Account.OPTION_REGISTER)
 608                    && nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
 609                processIq(nextTag);
 610            } else if (!isSecure() || this.loginInfo == null) {
 611                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 612            } else if (nextTag.isStart("success")) {
 613                final Element success = tagReader.readElement(nextTag);
 614                if (processSuccess(success)) {
 615                    break;
 616                }
 617            } else if (nextTag.isStart("failure")) {
 618                final Element failure = tagReader.readElement(nextTag);
 619                processFailure(failure);
 620            } else if (nextTag.isStart("continue", Namespace.SASL_2)) {
 621                // two step sasl2 - we don’t support this yet
 622                throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT);
 623            } else if (nextTag.isStart("challenge")) {
 624                final Element challenge = tagReader.readElement(nextTag);
 625                processChallenge(challenge);
 626            } else if (!LoginInfo.isSuccess(this.loginInfo)) {
 627                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 628            } else if (this.streamId != null
 629                    && nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) {
 630                final Element resumed = tagReader.readElement(nextTag);
 631                processResumed(resumed);
 632            } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) {
 633                final Element failed = tagReader.readElement(nextTag);
 634                processFailed(failed, true);
 635            } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
 636                processIq(nextTag);
 637            } else if (!isBound) {
 638                Log.d(
 639                        Config.LOGTAG,
 640                        account.getJid().asBareJid()
 641                                + ": server sent unexpected"
 642                                + nextTag.identifier());
 643                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 644            } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) {
 645                processMessage(nextTag);
 646            } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) {
 647                processPresence(nextTag);
 648            } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) {
 649                final Element enabled = tagReader.readElement(nextTag);
 650                processEnabled(enabled);
 651            } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) {
 652                tagReader.readElement(nextTag);
 653                if (Config.EXTENDED_SM_LOGGING) {
 654                    Log.d(
 655                            Config.LOGTAG,
 656                            account.getJid().asBareJid()
 657                                    + ": acknowledging stanza #"
 658                                    + this.stanzasReceived);
 659                }
 660                final AckPacket ack = new AckPacket(this.stanzasReceived);
 661                tagWriter.writeStanzaAsync(ack);
 662            } else if (nextTag.isStart("a", Namespace.STREAM_MANAGEMENT)) {
 663                boolean accountUiNeedsRefresh = false;
 664                synchronized (NotificationService.CATCHUP_LOCK) {
 665                    if (mWaitingForSmCatchup.compareAndSet(true, false)) {
 666                        final int messageCount = mSmCatchupMessageCounter.get();
 667                        final int pendingIQs = packetCallbacks.size();
 668                        Log.d(
 669                                Config.LOGTAG,
 670                                account.getJid().asBareJid()
 671                                        + ": SM catchup complete (messages="
 672                                        + messageCount
 673                                        + ", pending IQs="
 674                                        + pendingIQs
 675                                        + ")");
 676                        accountUiNeedsRefresh = true;
 677                        if (messageCount > 0) {
 678                            mXmppConnectionService
 679                                    .getNotificationService()
 680                                    .finishBacklog(true, account);
 681                        }
 682                    }
 683                }
 684                if (accountUiNeedsRefresh) {
 685                    mXmppConnectionService.updateAccountUi();
 686                }
 687                final Element ack = tagReader.readElement(nextTag);
 688                lastPacketReceived = SystemClock.elapsedRealtime();
 689                final boolean acknowledgedMessages;
 690                synchronized (this.mStanzaQueue) {
 691                    final Optional<Integer> serverSequence = ack.getOptionalIntAttribute("h");
 692                    if (serverSequence.isPresent()) {
 693                        acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence.get());
 694                    } else {
 695                        acknowledgedMessages = false;
 696                        Log.d(
 697                                Config.LOGTAG,
 698                                account.getJid().asBareJid()
 699                                        + ": server send ack without sequence number");
 700                    }
 701                }
 702                if (acknowledgedMessages) {
 703                    mXmppConnectionService.updateConversationUi();
 704                }
 705            } else {
 706                Log.e(
 707                        Config.LOGTAG,
 708                        account.getJid().asBareJid()
 709                                + ": Encountered unknown stream element"
 710                                + nextTag.identifier());
 711                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 712            }
 713            nextTag = tagReader.readTag();
 714        }
 715        if (nextTag != null && nextTag.isEnd("stream")) {
 716            streamCountDownLatch.countDown();
 717        }
 718    }
 719
 720    private void processChallenge(final Element challenge) throws IOException {
 721        final SaslMechanism.Version version;
 722        try {
 723            version = SaslMechanism.Version.of(challenge);
 724        } catch (final IllegalArgumentException e) {
 725            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 726        }
 727        final Element response;
 728        if (version == SaslMechanism.Version.SASL) {
 729            response = new Element("response", Namespace.SASL);
 730        } else if (version == SaslMechanism.Version.SASL_2) {
 731            response = new Element("response", Namespace.SASL_2);
 732        } else {
 733            throw new AssertionError("Missing implementation for " + version);
 734        }
 735        final LoginInfo currentLoginInfo = this.loginInfo;
 736        if (currentLoginInfo == null || LoginInfo.isSuccess(currentLoginInfo)) {
 737            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 738        }
 739        try {
 740            response.setContent(
 741                    currentLoginInfo.saslMechanism.getResponse(
 742                            challenge.getContent(), sslSocketOrNull(socket)));
 743        } catch (final SaslMechanism.AuthenticationException e) {
 744            // TODO: Send auth abort tag.
 745            Log.e(Config.LOGTAG, e.toString());
 746            throw new StateChangingException(Account.State.UNAUTHORIZED);
 747        }
 748        tagWriter.writeElement(response);
 749    }
 750
 751    private boolean processSuccess(final Element success)
 752            throws IOException, XmlPullParserException {
 753        final SaslMechanism.Version version;
 754        try {
 755            version = SaslMechanism.Version.of(success);
 756        } catch (final IllegalArgumentException e) {
 757            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 758        }
 759        final LoginInfo currentLoginInfo = this.loginInfo;
 760        final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo);
 761        if (currentLoginInfo == null || currentSaslMechanism == null) {
 762            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 763        }
 764        final String challenge;
 765        if (version == SaslMechanism.Version.SASL) {
 766            challenge = success.getContent();
 767        } else if (version == SaslMechanism.Version.SASL_2) {
 768            challenge = success.findChildContent("additional-data");
 769        } else {
 770            throw new AssertionError("Missing implementation for " + version);
 771        }
 772        try {
 773            currentLoginInfo.success(challenge, sslSocketOrNull(socket));
 774        } catch (final SaslMechanism.AuthenticationException e) {
 775            Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": authentication failure ", e);
 776            throw new StateChangingException(Account.State.UNAUTHORIZED);
 777        }
 778        Log.d(
 779                Config.LOGTAG,
 780                account.getJid().asBareJid().toString() + ": logged in (using " + version + ")");
 781        if (SaslMechanism.pin(currentSaslMechanism)) {
 782            account.setPinnedMechanism(currentSaslMechanism);
 783        }
 784        if (version == SaslMechanism.Version.SASL_2) {
 785            final String authorizationIdentifier =
 786                    success.findChildContent("authorization-identifier");
 787            final Jid authorizationJid;
 788            try {
 789                authorizationJid =
 790                        Strings.isNullOrEmpty(authorizationIdentifier)
 791                                ? null
 792                                : Jid.ofEscaped(authorizationIdentifier);
 793            } catch (final IllegalArgumentException e) {
 794                Log.d(
 795                        Config.LOGTAG,
 796                        account.getJid().asBareJid()
 797                                + ": SASL 2.0 authorization identifier was not a valid jid");
 798                throw new StateChangingException(Account.State.BIND_FAILURE);
 799            }
 800            if (authorizationJid == null) {
 801                throw new StateChangingException(Account.State.BIND_FAILURE);
 802            }
 803            Log.d(
 804                    Config.LOGTAG,
 805                    account.getJid().asBareJid()
 806                            + ": SASL 2.0 authorization identifier was "
 807                            + authorizationJid);
 808            if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) {
 809                Log.d(
 810                        Config.LOGTAG,
 811                        account.getJid().asBareJid()
 812                                + ": server tried to re-assign domain to "
 813                                + authorizationJid.getDomain());
 814                throw new StateChangingError(Account.State.BIND_FAILURE);
 815            }
 816            if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) {
 817                Log.d(
 818                        Config.LOGTAG,
 819                        account.getJid().asBareJid()
 820                                + ": jid changed during SASL 2.0. updating database");
 821            }
 822            final Element bound = success.findChild("bound", Namespace.BIND2);
 823            final Element resumed = success.findChild("resumed", Namespace.STREAM_MANAGEMENT);
 824            final Element failed = success.findChild("failed", Namespace.STREAM_MANAGEMENT);
 825            final Element tokenWrapper = success.findChild("token", Namespace.FAST);
 826            final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token");
 827            if (bound != null && resumed != null) {
 828                Log.d(
 829                        Config.LOGTAG,
 830                        account.getJid().asBareJid()
 831                                + ": server sent bound and resumed in SASL2 success");
 832                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 833            }
 834            if (resumed != null && streamId != null) {
 835                if (this.boundStreamFeatures != null) {
 836                    this.streamFeatures = this.boundStreamFeatures;
 837                    Log.d(
 838                            Config.LOGTAG,
 839                            "putting previous stream features back in place: "
 840                                    + XmlHelper.printElementNames(this.boundStreamFeatures));
 841                }
 842                processResumed(resumed);
 843            } else if (failed != null) {
 844                processFailed(failed, false); // wait for new stream features
 845            }
 846            if (bound != null) {
 847                clearIqCallbacks();
 848                this.isBound = true;
 849                processNopStreamFeatures();
 850                this.boundStreamFeatures = this.streamFeatures;
 851                final Element streamManagementEnabled =
 852                        bound.findChild("enabled", Namespace.STREAM_MANAGEMENT);
 853                final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
 854                final boolean waitForDisco;
 855                if (streamManagementEnabled != null) {
 856                    resetOutboundStanzaQueue();
 857                    processEnabled(streamManagementEnabled);
 858                    waitForDisco = true;
 859                } else {
 860                    // if we did not enable stream management in bind do it now
 861                    waitForDisco = enableStreamManagement();
 862                }
 863                final boolean negotiatedCarbons;
 864                if (carbonsEnabled != null) {
 865                    negotiatedCarbons = true;
 866                    Log.d(
 867                            Config.LOGTAG,
 868                            account.getJid().asBareJid()
 869                                    + ": successfully enabled carbons (via Bind 2.0)");
 870                    features.carbonsEnabled = true;
 871                } else if (loginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) {
 872                    negotiatedCarbons = true;
 873                    Log.d(
 874                            Config.LOGTAG,
 875                            account.getJid().asBareJid()
 876                                    + ": successfully enabled carbons (via Bind 2.0/implicit)");
 877                    features.carbonsEnabled = true;
 878                } else {
 879                    negotiatedCarbons = false;
 880                }
 881                sendPostBindInitialization(waitForDisco, negotiatedCarbons);
 882            }
 883            final HashedToken.Mechanism tokenMechanism;
 884            if (SaslMechanism.hashedToken(currentSaslMechanism)) {
 885                tokenMechanism = ((HashedToken) currentSaslMechanism).getTokenMechanism();
 886            } else if (this.hashTokenRequest != null) {
 887                tokenMechanism = this.hashTokenRequest;
 888            } else {
 889                tokenMechanism = null;
 890            }
 891            if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) {
 892                if (ChannelBinding.priority(tokenMechanism.channelBinding)
 893                        >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) {
 894                    this.account.setFastToken(tokenMechanism, token);
 895                    Log.d(
 896                            Config.LOGTAG,
 897                            account.getJid().asBareJid()
 898                                    + ": storing hashed token "
 899                                    + tokenMechanism);
 900                } else {
 901                    Log.d(
 902                            Config.LOGTAG,
 903                            account.getJid().asBareJid()
 904                                    + ": not accepting hashed token "
 905                                    + tokenMechanism.name()
 906                                    + " for log in mechanism "
 907                                    + currentSaslMechanism.getMechanism());
 908                    this.account.resetFastToken();
 909                }
 910            } else if (this.hashTokenRequest != null) {
 911                Log.w(
 912                        Config.LOGTAG,
 913                        account.getJid().asBareJid()
 914                                + ": no response to our hashed token request "
 915                                + this.hashTokenRequest);
 916            }
 917        }
 918        mXmppConnectionService.databaseBackend.updateAccount(account);
 919        this.quickStartInProgress = false;
 920        if (version == SaslMechanism.Version.SASL) {
 921            tagReader.reset();
 922            sendStartStream(false, true);
 923            final Tag tag = tagReader.readTag();
 924            if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
 925                processStream();
 926                return true;
 927            } else {
 928                throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
 929            }
 930        } else {
 931            return false;
 932        }
 933    }
 934
 935    private void resetOutboundStanzaQueue() {
 936        synchronized (this.mStanzaQueue) {
 937            final List<AbstractAcknowledgeableStanza> intermediateStanzas = new ArrayList<>();
 938            if (Config.EXTENDED_SM_LOGGING) {
 939                Log.d(
 940                        Config.LOGTAG,
 941                        account.getJid().asBareJid()
 942                                + ": stanzas sent before auth: "
 943                                + this.stanzasSentBeforeAuthentication);
 944            }
 945            for (int i = this.stanzasSentBeforeAuthentication + 1; i <= this.stanzasSent; ++i) {
 946                final AbstractAcknowledgeableStanza stanza = this.mStanzaQueue.get(i);
 947                if (stanza != null) {
 948                    intermediateStanzas.add(stanza);
 949                }
 950            }
 951            this.mStanzaQueue.clear();
 952            for (int i = 0; i < intermediateStanzas.size(); ++i) {
 953                this.mStanzaQueue.put(i, intermediateStanzas.get(i));
 954            }
 955            this.stanzasSent = intermediateStanzas.size();
 956            if (Config.EXTENDED_SM_LOGGING) {
 957                Log.d(
 958                        Config.LOGTAG,
 959                        account.getJid().asBareJid()
 960                                + ": resetting outbound stanza queue to "
 961                                + this.stanzasSent);
 962            }
 963        }
 964    }
 965
 966    private void processNopStreamFeatures() throws IOException {
 967        final Tag tag = tagReader.readTag();
 968        if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
 969            this.streamFeatures = tagReader.readElement(tag);
 970            Log.d(
 971                    Config.LOGTAG,
 972                    account.getJid().asBareJid()
 973                            + ": processed NOP stream features after success: "
 974                            + XmlHelper.printElementNames(this.streamFeatures));
 975        } else {
 976            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received " + tag);
 977            Log.d(
 978                    Config.LOGTAG,
 979                    account.getJid().asBareJid()
 980                            + ": server did not send stream features after SASL2 success");
 981            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 982        }
 983    }
 984
 985    private void processFailure(final Element failure) throws IOException {
 986        final SaslMechanism.Version version;
 987        try {
 988            version = SaslMechanism.Version.of(failure);
 989        } catch (final IllegalArgumentException e) {
 990            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 991        }
 992        Log.d(Config.LOGTAG, failure.toString());
 993        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
 994        if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
 995            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resetting token");
 996            account.resetFastToken();
 997            mXmppConnectionService.databaseBackend.updateAccount(account);
 998        }
 999        if (failure.hasChild("temporary-auth-failure")) {
1000            throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE);
1001        } else if (failure.hasChild("account-disabled")) {
1002            final String text = failure.findChildContent("text");
1003            if (Strings.isNullOrEmpty(text)) {
1004                throw new StateChangingException(Account.State.UNAUTHORIZED);
1005            }
1006            final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
1007            if (matcher.find()) {
1008                final HttpUrl url;
1009                try {
1010                    url = HttpUrl.get(text.substring(matcher.start(), matcher.end()));
1011                } catch (final IllegalArgumentException e) {
1012                    throw new StateChangingException(Account.State.UNAUTHORIZED);
1013                }
1014                if (url.isHttps()) {
1015                    this.redirectionUrl = url;
1016                    throw new StateChangingException(Account.State.PAYMENT_REQUIRED);
1017                }
1018            }
1019        }
1020        if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
1021            Log.d(
1022                    Config.LOGTAG,
1023                    account.getJid().asBareJid()
1024                            + ": fast authentication failed. falling back to regular authentication");
1025            authenticate();
1026        } else {
1027            throw new StateChangingException(Account.State.UNAUTHORIZED);
1028        }
1029    }
1030
1031    private static SSLSocket sslSocketOrNull(final Socket socket) {
1032        if (socket instanceof SSLSocket) {
1033            return (SSLSocket) socket;
1034        } else {
1035            return null;
1036        }
1037    }
1038
1039    private void processEnabled(final Element enabled) {
1040        final String id;
1041        if (enabled.getAttributeAsBoolean("resume")) {
1042            id = enabled.getAttribute("id");
1043        } else {
1044            id = null;
1045        }
1046        final String locationAttribute = enabled.getAttribute("location");
1047        final Resolver.Result currentResolverResult = this.currentResolverResult;
1048        final Resolver.Result location;
1049        if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) {
1050            location = null;
1051        } else {
1052            location = currentResolverResult.seeOtherHost(locationAttribute);
1053        }
1054        final StreamId streamId = id == null ? null : new StreamId(id, location);
1055        if (streamId == null) {
1056            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream management enabled");
1057        } else {
1058            Log.d(
1059                    Config.LOGTAG,
1060                    account.getJid().asBareJid()
1061                            + ": stream management enabled. resume at: "
1062                            + streamId.location);
1063        }
1064        this.streamId = streamId;
1065        this.stanzasReceived = 0;
1066        this.inSmacksSession = true;
1067        final RequestPacket r = new RequestPacket();
1068        tagWriter.writeStanzaAsync(r);
1069    }
1070
1071    private void processResumed(final Element resumed) throws StateChangingException {
1072        this.inSmacksSession = true;
1073        this.isBound = true;
1074        this.tagWriter.writeStanzaAsync(new RequestPacket());
1075        lastPacketReceived = SystemClock.elapsedRealtime();
1076        final Optional<Integer> h = resumed.getOptionalIntAttribute("h");
1077        final int serverCount;
1078        if (h.isPresent()) {
1079            serverCount = h.get();
1080        } else {
1081            resetStreamId();
1082            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1083        }
1084        final ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
1085        final boolean acknowledgedMessages;
1086        synchronized (this.mStanzaQueue) {
1087            if (serverCount < stanzasSent) {
1088                Log.d(
1089                        Config.LOGTAG,
1090                        account.getJid().asBareJid() + ": session resumed with lost packages");
1091                stanzasSent = serverCount;
1092            } else {
1093                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed");
1094            }
1095            acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
1096            for (int i = 0; i < this.mStanzaQueue.size(); ++i) {
1097                failedStanzas.add(mStanzaQueue.valueAt(i));
1098            }
1099            mStanzaQueue.clear();
1100        }
1101        if (acknowledgedMessages) {
1102            mXmppConnectionService.updateConversationUi();
1103        }
1104        Log.d(
1105                Config.LOGTAG,
1106                account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas");
1107        for (final AbstractAcknowledgeableStanza packet : failedStanzas) {
1108            if (packet instanceof MessagePacket message) {
1109                mXmppConnectionService.markMessage(
1110                        account,
1111                        message.getTo().asBareJid(),
1112                        message.getId(),
1113                        Message.STATUS_UNSEND);
1114            }
1115            sendPacket(packet);
1116        }
1117        changeStatusToOnline();
1118    }
1119
1120    private void changeStatusToOnline() {
1121        Log.d(
1122                Config.LOGTAG,
1123                account.getJid().asBareJid() + ": online with resource " + account.getResource());
1124        changeStatus(Account.State.ONLINE);
1125    }
1126
1127    private void processFailed(final Element failed, final boolean sendBindRequest) {
1128        final Optional<Integer> serverCount = failed.getOptionalIntAttribute("h");
1129        if (serverCount.isPresent()) {
1130            Log.d(
1131                    Config.LOGTAG,
1132                    account.getJid().asBareJid()
1133                            + ": resumption failed but server acknowledged stanza #"
1134                            + serverCount.get());
1135            final boolean acknowledgedMessages;
1136            synchronized (this.mStanzaQueue) {
1137                acknowledgedMessages = acknowledgeStanzaUpTo(serverCount.get());
1138            }
1139            if (acknowledgedMessages) {
1140                mXmppConnectionService.updateConversationUi();
1141            }
1142        } else {
1143            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed");
1144        }
1145        resetStreamId();
1146        if (sendBindRequest) {
1147            sendBindRequest();
1148        }
1149    }
1150
1151    private boolean acknowledgeStanzaUpTo(final int serverCount) {
1152        if (serverCount > stanzasSent) {
1153            Log.e(
1154                    Config.LOGTAG,
1155                    "server acknowledged more stanzas than we sent. serverCount="
1156                            + serverCount
1157                            + ", ourCount="
1158                            + stanzasSent);
1159        }
1160        boolean acknowledgedMessages = false;
1161        for (int i = 0; i < mStanzaQueue.size(); ++i) {
1162            if (serverCount >= mStanzaQueue.keyAt(i)) {
1163                if (Config.EXTENDED_SM_LOGGING) {
1164                    Log.d(
1165                            Config.LOGTAG,
1166                            account.getJid().asBareJid()
1167                                    + ": server acknowledged stanza #"
1168                                    + mStanzaQueue.keyAt(i));
1169                }
1170                final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
1171                if (stanza instanceof MessagePacket packet && acknowledgedListener != null) {
1172                    final String id = packet.getId();
1173                    final Jid to = packet.getTo();
1174                    if (id != null && to != null) {
1175                        acknowledgedMessages |=
1176                                acknowledgedListener.onMessageAcknowledged(account, to, id);
1177                    }
1178                }
1179                mStanzaQueue.removeAt(i);
1180                i--;
1181            }
1182        }
1183        return acknowledgedMessages;
1184    }
1185
1186    private @NonNull Element processPacket(final Tag currentTag, final int packetType)
1187            throws IOException {
1188        final Element element =
1189                switch (packetType) {
1190                    case PACKET_IQ -> new IqPacket();
1191                    case PACKET_MESSAGE -> new MessagePacket();
1192                    case PACKET_PRESENCE -> new PresencePacket();
1193                    default -> throw new AssertionError("Should never encounter invalid type");
1194                };
1195        element.setAttributes(currentTag.getAttributes());
1196        Tag nextTag = tagReader.readTag();
1197        if (nextTag == null) {
1198            throw new IOException("interrupted mid tag");
1199        }
1200        while (!nextTag.isEnd(element.getName())) {
1201            if (!nextTag.isNo()) {
1202                element.addChild(tagReader.readElement(nextTag));
1203            }
1204            nextTag = tagReader.readTag();
1205            if (nextTag == null) {
1206                throw new IOException("interrupted mid tag");
1207            }
1208        }
1209        if (stanzasReceived == Integer.MAX_VALUE) {
1210            resetStreamId();
1211            throw new IOException("time to restart the session. cant handle >2 billion pcks");
1212        }
1213        if (inSmacksSession) {
1214            ++stanzasReceived;
1215        } else if (features.sm()) {
1216            Log.d(
1217                    Config.LOGTAG,
1218                    account.getJid().asBareJid()
1219                            + ": not counting stanza("
1220                            + element.getClass().getSimpleName()
1221                            + "). Not in smacks session.");
1222        }
1223        lastPacketReceived = SystemClock.elapsedRealtime();
1224        if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
1225            Log.d(Config.LOGTAG, "[background stanza] " + element);
1226        }
1227        if (element instanceof IqPacket
1228                && (((IqPacket) element).getType() == IqPacket.TYPE.SET)
1229                && element.hasChild("jingle", Namespace.JINGLE)) {
1230            return JinglePacket.upgrade((IqPacket) element);
1231        } else {
1232            return element;
1233        }
1234    }
1235
1236    private void processIq(final Tag currentTag) throws IOException {
1237        final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
1238        if (!packet.valid()) {
1239            Log.e(
1240                    Config.LOGTAG,
1241                    "encountered invalid iq from='"
1242                            + packet.getFrom()
1243                            + "' to='"
1244                            + packet.getTo()
1245                            + "'");
1246            return;
1247        }
1248        if (Thread.currentThread().isInterrupted()) {
1249            Log.d(
1250                    Config.LOGTAG,
1251                    account.getJid().asBareJid() + "Not processing iq. Thread was interrupted");
1252            return;
1253        }
1254        if (packet instanceof JinglePacket jinglePacket && isBound) {
1255            if (this.jingleListener != null) {
1256                this.jingleListener.onJinglePacketReceived(account, jinglePacket);
1257            }
1258        } else {
1259            final OnIqPacketReceived callback = getIqPacketReceivedCallback(packet);
1260            if (callback == null) {
1261                Log.d(
1262                        Config.LOGTAG,
1263                        account.getJid().asBareJid().toString()
1264                                + ": no callback registered for IQ from "
1265                                + packet.getFrom());
1266                return;
1267            }
1268            try {
1269                callback.onIqPacketReceived(account, packet);
1270            } catch (final StateChangingError error) {
1271                throw new StateChangingException(error.state);
1272            }
1273        }
1274    }
1275
1276    private OnIqPacketReceived getIqPacketReceivedCallback(final IqPacket stanza)
1277            throws StateChangingException {
1278        final boolean isRequest =
1279                stanza.getType() == IqPacket.TYPE.GET || stanza.getType() == IqPacket.TYPE.SET;
1280        if (isRequest) {
1281            if (isBound) {
1282                return this.unregisteredIqListener;
1283            } else {
1284                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1285            }
1286        } else {
1287            synchronized (this.packetCallbacks) {
1288                final var pair = packetCallbacks.get(stanza.getId());
1289                if (pair == null) {
1290                    return null;
1291                }
1292                if (pair.first.toServer(account)) {
1293                    if (stanza.fromServer(account)) {
1294                        packetCallbacks.remove(stanza.getId());
1295                        return pair.second;
1296                    } else {
1297                        Log.e(
1298                                Config.LOGTAG,
1299                                account.getJid().asBareJid().toString()
1300                                        + ": ignoring spoofed iq packet");
1301                    }
1302                } else {
1303                    if (stanza.getFrom() != null && stanza.getFrom().equals(pair.first.getTo())) {
1304                        packetCallbacks.remove(stanza.getId());
1305                        return pair.second;
1306                    } else {
1307                        Log.e(
1308                                Config.LOGTAG,
1309                                account.getJid().asBareJid().toString()
1310                                        + ": ignoring spoofed iq packet");
1311                    }
1312                }
1313            }
1314        }
1315        return null;
1316    }
1317
1318    private void processMessage(final Tag currentTag) throws IOException {
1319        final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
1320        if (!packet.valid()) {
1321            Log.e(
1322                    Config.LOGTAG,
1323                    "encountered invalid message from='"
1324                            + packet.getFrom()
1325                            + "' to='"
1326                            + packet.getTo()
1327                            + "'");
1328            return;
1329        }
1330        if (Thread.currentThread().isInterrupted()) {
1331            Log.d(
1332                    Config.LOGTAG,
1333                    account.getJid().asBareJid()
1334                            + "Not processing message. Thread was interrupted");
1335            return;
1336        }
1337        this.messageListener.onMessagePacketReceived(account, packet);
1338    }
1339
1340    private void processPresence(final Tag currentTag) throws IOException {
1341        final PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
1342        if (!packet.valid()) {
1343            Log.e(
1344                    Config.LOGTAG,
1345                    "encountered invalid presence from='"
1346                            + packet.getFrom()
1347                            + "' to='"
1348                            + packet.getTo()
1349                            + "'");
1350            return;
1351        }
1352        if (Thread.currentThread().isInterrupted()) {
1353            Log.d(
1354                    Config.LOGTAG,
1355                    account.getJid().asBareJid()
1356                            + "Not processing presence. Thread was interrupted");
1357            return;
1358        }
1359        this.presenceListener.onPresencePacketReceived(account, packet);
1360    }
1361
1362    private void sendStartTLS() throws IOException {
1363        final Tag startTLS = Tag.empty("starttls");
1364        startTLS.setAttribute("xmlns", Namespace.TLS);
1365        tagWriter.writeTag(startTLS);
1366    }
1367
1368    private void switchOverToTls() throws XmlPullParserException, IOException {
1369        tagReader.readTag();
1370        final Socket socket = this.socket;
1371        final SSLSocket sslSocket = upgradeSocketToTls(socket);
1372        this.socket = sslSocket;
1373        this.tagReader.setInputStream(sslSocket.getInputStream());
1374        this.tagWriter.setOutputStream(sslSocket.getOutputStream());
1375        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
1376        final boolean quickStart;
1377        try {
1378            quickStart = establishStream(SSLSockets.version(sslSocket));
1379        } catch (final InterruptedException e) {
1380            return;
1381        }
1382        if (quickStart) {
1383            this.quickStartInProgress = true;
1384        }
1385        features.encryptionEnabled = true;
1386        final Tag tag = tagReader.readTag();
1387        if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
1388            SSLSockets.log(account, sslSocket);
1389            processStream();
1390        } else {
1391            throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
1392        }
1393        sslSocket.close();
1394    }
1395
1396    private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
1397        final SSLSocketFactory sslSocketFactory;
1398        try {
1399            sslSocketFactory = getSSLSocketFactory();
1400        } catch (final NoSuchAlgorithmException | KeyManagementException e) {
1401            throw new StateChangingException(Account.State.TLS_ERROR);
1402        }
1403        final InetAddress address = socket.getInetAddress();
1404        final SSLSocket sslSocket =
1405                (SSLSocket)
1406                        sslSocketFactory.createSocket(
1407                                socket, address.getHostAddress(), socket.getPort(), true);
1408        SSLSockets.setSecurity(sslSocket);
1409        SSLSockets.setHostname(sslSocket, IDN.toASCII(account.getServer()));
1410        SSLSockets.setApplicationProtocol(sslSocket, "xmpp-client");
1411        final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
1412        try {
1413            if (!xmppDomainVerifier.verify(
1414                    account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
1415                Log.d(
1416                        Config.LOGTAG,
1417                        account.getJid().asBareJid()
1418                                + ": TLS certificate domain verification failed");
1419                FileBackend.close(sslSocket);
1420                throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN);
1421            }
1422        } catch (final SSLPeerUnverifiedException e) {
1423            FileBackend.close(sslSocket);
1424            throw new StateChangingException(Account.State.TLS_ERROR);
1425        }
1426        return sslSocket;
1427    }
1428
1429    private void processStreamFeatures(final Tag currentTag) throws IOException {
1430        this.streamFeatures = tagReader.readElement(currentTag);
1431        final boolean isSecure = isSecure();
1432        final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
1433        if (this.quickStartInProgress) {
1434            if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
1435                Log.d(
1436                        Config.LOGTAG,
1437                        account.getJid().asBareJid()
1438                                + ": quick start in progress. ignoring features: "
1439                                + XmlHelper.printElementNames(this.streamFeatures));
1440                if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
1441                    return;
1442                }
1443                if (isFastTokenAvailable(
1444                        this.streamFeatures.findChild("authentication", Namespace.SASL_2))) {
1445                    Log.d(
1446                            Config.LOGTAG,
1447                            account.getJid().asBareJid()
1448                                    + ": fast token available; resetting quick start");
1449                    account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1450                    mXmppConnectionService.databaseBackend.updateAccount(account);
1451                }
1452                return;
1453            }
1454            Log.d(
1455                    Config.LOGTAG,
1456                    account.getJid().asBareJid()
1457                            + ": server lost support for SASL 2. quick start not possible");
1458            this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1459            mXmppConnectionService.databaseBackend.updateAccount(account);
1460            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1461        }
1462        if (this.streamFeatures.hasChild("starttls", Namespace.TLS)
1463                && !features.encryptionEnabled) {
1464            sendStartTLS();
1465        } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1466                && account.isOptionSet(Account.OPTION_REGISTER)) {
1467            if (isSecure) {
1468                register();
1469            } else {
1470                Log.d(
1471                        Config.LOGTAG,
1472                        account.getJid().asBareJid()
1473                                + ": unable to find STARTTLS for registration process "
1474                                + XmlHelper.printElementNames(this.streamFeatures));
1475                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1476            }
1477        } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1478                && account.isOptionSet(Account.OPTION_REGISTER)) {
1479            throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
1480        } else if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)
1481                && shouldAuthenticate
1482                && isSecure) {
1483            authenticate(SaslMechanism.Version.SASL_2);
1484        } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL)
1485                && shouldAuthenticate
1486                && isSecure) {
1487            authenticate(SaslMechanism.Version.SASL);
1488        } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)
1489                && isSecure
1490                && LoginInfo.isSuccess(loginInfo)
1491                && streamId != null
1492                && !inSmacksSession) {
1493            if (Config.EXTENDED_SM_LOGGING) {
1494                Log.d(
1495                        Config.LOGTAG,
1496                        account.getJid().asBareJid()
1497                                + ": resuming after stanza #"
1498                                + stanzasReceived);
1499            }
1500            final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived);
1501            this.mSmCatchupMessageCounter.set(0);
1502            this.mWaitingForSmCatchup.set(true);
1503            this.tagWriter.writeStanzaAsync(resume);
1504        } else if (needsBinding) {
1505            if (this.streamFeatures.hasChild("bind", Namespace.BIND)
1506                    && isSecure
1507                    && LoginInfo.isSuccess(loginInfo)) {
1508                sendBindRequest();
1509            } else {
1510                Log.d(
1511                        Config.LOGTAG,
1512                        account.getJid().asBareJid()
1513                                + ": unable to find bind feature "
1514                                + XmlHelper.printElementNames(this.streamFeatures));
1515                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1516            }
1517        } else {
1518            Log.d(
1519                    Config.LOGTAG,
1520                    account.getJid().asBareJid()
1521                            + ": received NOP stream features: "
1522                            + XmlHelper.printElementNames(this.streamFeatures));
1523        }
1524    }
1525
1526    private void authenticate() throws IOException {
1527        final boolean isSecure = isSecure();
1528        if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
1529            authenticate(SaslMechanism.Version.SASL_2);
1530        } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) {
1531            authenticate(SaslMechanism.Version.SASL);
1532        } else {
1533            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1534        }
1535    }
1536
1537    private boolean isSecure() {
1538        return features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
1539    }
1540
1541    private void authenticate(final SaslMechanism.Version version) throws IOException {
1542        final Element authElement;
1543        if (version == SaslMechanism.Version.SASL) {
1544            authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL);
1545        } else {
1546            authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2);
1547        }
1548        final Collection<String> mechanisms = SaslMechanism.mechanisms(authElement);
1549        final Element cbElement =
1550                this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
1551        final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbElement);
1552        final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
1553        final SaslMechanism saslMechanism =
1554                factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
1555        this.validate(saslMechanism, mechanisms);
1556        final boolean quickStartAvailable;
1557        final String firstMessage =
1558                saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
1559        final boolean usingFast = SaslMechanism.hashedToken(saslMechanism);
1560        final Element authenticate;
1561        if (version == SaslMechanism.Version.SASL) {
1562            authenticate = new Element("auth", Namespace.SASL);
1563            if (!Strings.isNullOrEmpty(firstMessage)) {
1564                authenticate.setContent(firstMessage);
1565            }
1566            quickStartAvailable = false;
1567            this.loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList());
1568        } else if (version == SaslMechanism.Version.SASL_2) {
1569            final Element inline = authElement.findChild("inline", Namespace.SASL_2);
1570            final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT);
1571            final HashedToken.Mechanism hashTokenRequest;
1572            if (usingFast) {
1573                hashTokenRequest = null;
1574            } else {
1575                final Element fast =
1576                        inline == null ? null : inline.findChild("fast", Namespace.FAST);
1577                final Collection<String> fastMechanisms = SaslMechanism.mechanisms(fast);
1578                hashTokenRequest =
1579                        HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket));
1580            }
1581            final Collection<String> bindFeatures = Bind2.features(inline);
1582            quickStartAvailable =
1583                    sm
1584                            && bindFeatures != null
1585                            && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES);
1586            if (bindFeatures != null) {
1587                try {
1588                    mXmppConnectionService.restoredFromDatabaseLatch.await();
1589                } catch (final InterruptedException e) {
1590                    Log.d(
1591                            Config.LOGTAG,
1592                            account.getJid().asBareJid()
1593                                    + ": interrupted while waiting for DB restore during SASL2 bind");
1594                    return;
1595                }
1596            }
1597            this.loginInfo = new LoginInfo(saslMechanism, version, bindFeatures);
1598            this.hashTokenRequest = hashTokenRequest;
1599            authenticate =
1600                    generateAuthenticationRequest(
1601                            firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
1602        } else {
1603            throw new AssertionError("Missing implementation for " + version);
1604        }
1605
1606        if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) {
1607            mXmppConnectionService.databaseBackend.updateAccount(account);
1608        }
1609
1610        Log.d(
1611                Config.LOGTAG,
1612                account.getJid().toString()
1613                        + ": Authenticating with "
1614                        + version
1615                        + "/"
1616                        + LoginInfo.mechanism(this.loginInfo).getMechanism());
1617        authenticate.setAttribute("mechanism", LoginInfo.mechanism(this.loginInfo).getMechanism());
1618        synchronized (this.mStanzaQueue) {
1619            this.stanzasSentBeforeAuthentication = this.stanzasSent;
1620            tagWriter.writeElement(authenticate);
1621        }
1622    }
1623
1624    private static boolean isFastTokenAvailable(final Element authentication) {
1625        final Element inline = authentication == null ? null : authentication.findChild("inline");
1626        return inline != null && inline.hasChild("fast", Namespace.FAST);
1627    }
1628
1629    private void validate(
1630            final @Nullable SaslMechanism saslMechanism, Collection<String> mechanisms)
1631            throws StateChangingException {
1632        if (saslMechanism == null) {
1633            Log.d(
1634                    Config.LOGTAG,
1635                    account.getJid().asBareJid()
1636                            + ": unable to find supported SASL mechanism in "
1637                            + mechanisms);
1638            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1639        }
1640        if (SaslMechanism.hashedToken(saslMechanism)) {
1641            return;
1642        }
1643        final int pinnedMechanism = account.getPinnedMechanismPriority();
1644        if (pinnedMechanism > saslMechanism.getPriority()) {
1645            Log.e(
1646                    Config.LOGTAG,
1647                    "Auth failed. Authentication mechanism "
1648                            + saslMechanism.getMechanism()
1649                            + " has lower priority ("
1650                            + saslMechanism.getPriority()
1651                            + ") than pinned priority ("
1652                            + pinnedMechanism
1653                            + "). Possible downgrade attack?");
1654            throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
1655        }
1656    }
1657
1658    private Element generateAuthenticationRequest(
1659            final String firstMessage, final boolean usingFast) {
1660        return generateAuthenticationRequest(
1661                firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
1662    }
1663
1664    private Element generateAuthenticationRequest(
1665            final String firstMessage,
1666            final boolean usingFast,
1667            final HashedToken.Mechanism hashedTokenRequest,
1668            final Collection<String> bind,
1669            final boolean inlineStreamManagement) {
1670        final Element authenticate = new Element("authenticate", Namespace.SASL_2);
1671        if (!Strings.isNullOrEmpty(firstMessage)) {
1672            authenticate.addChild("initial-response").setContent(firstMessage);
1673        }
1674        final Element userAgent = authenticate.addChild("user-agent");
1675        userAgent.setAttribute("id", AccountUtils.publicDeviceId(account));
1676        userAgent
1677                .addChild("software")
1678                .setContent(mXmppConnectionService.getString(R.string.app_name));
1679        if (!PhoneHelper.isEmulator()) {
1680            userAgent
1681                    .addChild("device")
1682                    .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
1683        }
1684        // do not include bind if 'inlineStreamManagement' is missing and we have a streamId
1685        // (because we would rather just do a normal SM/resume)
1686        final boolean mayAttemptBind = streamId == null || inlineStreamManagement;
1687        if (bind != null && mayAttemptBind) {
1688            authenticate.addChild(generateBindRequest(bind));
1689        }
1690        if (inlineStreamManagement && streamId != null) {
1691            final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived);
1692            this.mSmCatchupMessageCounter.set(0);
1693            this.mWaitingForSmCatchup.set(true);
1694            authenticate.addChild(resume);
1695        }
1696        if (hashedTokenRequest != null) {
1697            authenticate
1698                    .addChild("request-token", Namespace.FAST)
1699                    .setAttribute("mechanism", hashedTokenRequest.name());
1700        }
1701        if (usingFast) {
1702            authenticate.addChild("fast", Namespace.FAST);
1703        }
1704        return authenticate;
1705    }
1706
1707    private Element generateBindRequest(final Collection<String> bindFeatures) {
1708        Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
1709        final Element bind = new Element("bind", Namespace.BIND2);
1710        bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name));
1711        if (bindFeatures.contains(Namespace.CARBONS)) {
1712            bind.addChild("enable", Namespace.CARBONS);
1713        }
1714        if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
1715            bind.addChild(new EnablePacket());
1716        }
1717        return bind;
1718    }
1719
1720    private void register() {
1721        final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN);
1722        if (preAuth != null && features.invite()) {
1723            final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET);
1724            preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
1725            sendUnmodifiedIqPacket(
1726                    preAuthRequest,
1727                    (account, response) -> {
1728                        if (response.getType() == IqPacket.TYPE.RESULT) {
1729                            sendRegistryRequest();
1730                        } else {
1731                            final String error = response.getErrorCondition();
1732                            Log.d(
1733                                    Config.LOGTAG,
1734                                    account.getJid().asBareJid()
1735                                            + ": failed to pre auth. "
1736                                            + error);
1737                            throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
1738                        }
1739                    },
1740                    true);
1741        } else {
1742            sendRegistryRequest();
1743        }
1744    }
1745
1746    private void sendRegistryRequest() {
1747        final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
1748        register.query(Namespace.REGISTER);
1749        register.setTo(account.getDomain());
1750        sendUnmodifiedIqPacket(
1751                register,
1752                (account, packet) -> {
1753                    if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1754                        return;
1755                    }
1756                    if (packet.getType() == IqPacket.TYPE.ERROR) {
1757                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1758                    }
1759                    final Element query = packet.query(Namespace.REGISTER);
1760                    if (query.hasChild("username") && (query.hasChild("password"))) {
1761                        final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET);
1762                        final Element username =
1763                                new Element("username").setContent(account.getUsername());
1764                        final Element password =
1765                                new Element("password").setContent(account.getPassword());
1766                        register1.query(Namespace.REGISTER).addChild(username);
1767                        register1.query().addChild(password);
1768                        register1.setFrom(account.getJid().asBareJid());
1769                        sendUnmodifiedIqPacket(register1, registrationResponseListener, true);
1770                    } else if (query.hasChild("x", Namespace.DATA)) {
1771                        final Data data = Data.parse(query.findChild("x", Namespace.DATA));
1772                        final Element blob = query.findChild("data", "urn:xmpp:bob");
1773                        final String id = packet.getId();
1774                        InputStream is;
1775                        if (blob != null) {
1776                            try {
1777                                final String base64Blob = blob.getContent();
1778                                final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
1779                                is = new ByteArrayInputStream(strBlob);
1780                            } catch (Exception e) {
1781                                is = null;
1782                            }
1783                        } else {
1784                            final boolean useTor =
1785                                    mXmppConnectionService.useTorToConnect() || account.isOnion();
1786                            try {
1787                                final String url = data.getValue("url");
1788                                final String fallbackUrl = data.getValue("captcha-fallback-url");
1789                                if (url != null) {
1790                                    is = HttpConnectionManager.open(url, useTor);
1791                                } else if (fallbackUrl != null) {
1792                                    is = HttpConnectionManager.open(fallbackUrl, useTor);
1793                                } else {
1794                                    is = null;
1795                                }
1796                            } catch (final IOException e) {
1797                                Log.d(
1798                                        Config.LOGTAG,
1799                                        account.getJid().asBareJid() + ": unable to fetch captcha",
1800                                        e);
1801                                is = null;
1802                            }
1803                        }
1804
1805                        if (is != null) {
1806                            Bitmap captcha = BitmapFactory.decodeStream(is);
1807                            try {
1808                                if (mXmppConnectionService.displayCaptchaRequest(
1809                                        account, id, data, captcha)) {
1810                                    return;
1811                                }
1812                            } catch (Exception e) {
1813                                throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1814                            }
1815                        }
1816                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1817                    } else if (query.hasChild("instructions")
1818                            || query.hasChild("x", Namespace.OOB)) {
1819                        final String instructions = query.findChildContent("instructions");
1820                        final Element oob = query.findChild("x", Namespace.OOB);
1821                        final String url = oob == null ? null : oob.findChildContent("url");
1822                        if (url != null) {
1823                            setAccountCreationFailed(url);
1824                        } else if (instructions != null) {
1825                            final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
1826                            if (matcher.find()) {
1827                                setAccountCreationFailed(
1828                                        instructions.substring(matcher.start(), matcher.end()));
1829                            }
1830                        }
1831                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1832                    }
1833                },
1834                true);
1835    }
1836
1837    private void setAccountCreationFailed(final String url) {
1838        final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
1839        if (httpUrl != null && httpUrl.isHttps()) {
1840            this.redirectionUrl = httpUrl;
1841            throw new StateChangingError(Account.State.REGISTRATION_WEB);
1842        }
1843        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1844    }
1845
1846    public HttpUrl getRedirectionUrl() {
1847        return this.redirectionUrl;
1848    }
1849
1850    public void resetEverything() {
1851        resetAttemptCount(true);
1852        resetStreamId();
1853        clearIqCallbacks();
1854        synchronized (this.mStanzaQueue) {
1855            this.stanzasSent = 0;
1856            this.mStanzaQueue.clear();
1857        }
1858        this.redirectionUrl = null;
1859        synchronized (this.disco) {
1860            disco.clear();
1861        }
1862        synchronized (this.commands) {
1863            this.commands.clear();
1864        }
1865        this.loginInfo = null;
1866    }
1867
1868    private void sendBindRequest() {
1869        try {
1870            mXmppConnectionService.restoredFromDatabaseLatch.await();
1871        } catch (InterruptedException e) {
1872            Log.d(
1873                    Config.LOGTAG,
1874                    account.getJid().asBareJid()
1875                            + ": interrupted while waiting for DB restore during bind");
1876            return;
1877        }
1878        clearIqCallbacks();
1879        if (account.getJid().isBareJid()) {
1880            account.setResource(this.createNewResource());
1881        } else {
1882            fixResource(mXmppConnectionService, account);
1883        }
1884        final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1885        final String resource =
1886                Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource();
1887        iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource);
1888        this.sendUnmodifiedIqPacket(
1889                iq,
1890                (account, packet) -> {
1891                    if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1892                        return;
1893                    }
1894                    final Element bind = packet.findChild("bind");
1895                    if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
1896                        isBound = true;
1897                        final Element jid = bind.findChild("jid");
1898                        if (jid != null && jid.getContent() != null) {
1899                            try {
1900                                Jid assignedJid = Jid.ofEscaped(jid.getContent());
1901                                if (!account.getJid().getDomain().equals(assignedJid.getDomain())) {
1902                                    Log.d(
1903                                            Config.LOGTAG,
1904                                            account.getJid().asBareJid()
1905                                                    + ": server tried to re-assign domain to "
1906                                                    + assignedJid.getDomain());
1907                                    throw new StateChangingError(Account.State.BIND_FAILURE);
1908                                }
1909                                if (account.setJid(assignedJid)) {
1910                                    Log.d(
1911                                            Config.LOGTAG,
1912                                            account.getJid().asBareJid()
1913                                                    + ": jid changed during bind. updating database");
1914                                    mXmppConnectionService.databaseBackend.updateAccount(account);
1915                                }
1916                                if (streamFeatures.hasChild("session")
1917                                        && !streamFeatures
1918                                                .findChild("session")
1919                                                .hasChild("optional")) {
1920                                    sendStartSession();
1921                                } else {
1922                                    final boolean waitForDisco = enableStreamManagement();
1923                                    sendPostBindInitialization(waitForDisco, false);
1924                                }
1925                                return;
1926                            } catch (final IllegalArgumentException e) {
1927                                Log.d(
1928                                        Config.LOGTAG,
1929                                        account.getJid().asBareJid()
1930                                                + ": server reported invalid jid ("
1931                                                + jid.getContent()
1932                                                + ") on bind");
1933                            }
1934                        } else {
1935                            Log.d(
1936                                    Config.LOGTAG,
1937                                    account.getJid()
1938                                            + ": disconnecting because of bind failure. (no jid)");
1939                        }
1940                    } else {
1941                        Log.d(
1942                                Config.LOGTAG,
1943                                account.getJid()
1944                                        + ": disconnecting because of bind failure ("
1945                                        + packet);
1946                    }
1947                    final Element error = packet.findChild("error");
1948                    if (packet.getType() == IqPacket.TYPE.ERROR
1949                            && error != null
1950                            && error.hasChild("conflict")) {
1951                        account.setResource(createNewResource());
1952                    }
1953                    throw new StateChangingError(Account.State.BIND_FAILURE);
1954                },
1955                true);
1956    }
1957
1958    private void clearIqCallbacks() {
1959        final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
1960        final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
1961        synchronized (this.packetCallbacks) {
1962            if (this.packetCallbacks.size() == 0) {
1963                return;
1964            }
1965            Log.d(
1966                    Config.LOGTAG,
1967                    account.getJid().asBareJid()
1968                            + ": clearing "
1969                            + this.packetCallbacks.size()
1970                            + " iq callbacks");
1971            final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator =
1972                    this.packetCallbacks.values().iterator();
1973            while (iterator.hasNext()) {
1974                Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
1975                callbacks.add(entry.second);
1976                iterator.remove();
1977            }
1978        }
1979        for (OnIqPacketReceived callback : callbacks) {
1980            try {
1981                callback.onIqPacketReceived(account, failurePacket);
1982            } catch (StateChangingError error) {
1983                Log.d(
1984                        Config.LOGTAG,
1985                        account.getJid().asBareJid()
1986                                + ": caught StateChangingError("
1987                                + error.state.toString()
1988                                + ") while clearing callbacks");
1989                // ignore
1990            }
1991        }
1992        Log.d(
1993                Config.LOGTAG,
1994                account.getJid().asBareJid()
1995                        + ": done clearing iq callbacks. "
1996                        + this.packetCallbacks.size()
1997                        + " left");
1998    }
1999
2000    public void sendDiscoTimeout() {
2001        if (mWaitForDisco.compareAndSet(true, false)) {
2002            Log.d(
2003                    Config.LOGTAG,
2004                    account.getJid().asBareJid() + ": finalizing bind after disco timeout");
2005            finalizeBind();
2006        }
2007    }
2008
2009    private void sendStartSession() {
2010        Log.d(
2011                Config.LOGTAG,
2012                account.getJid().asBareJid() + ": sending legacy session to outdated server");
2013        final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
2014        startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
2015        this.sendUnmodifiedIqPacket(
2016                startSession,
2017                (account, packet) -> {
2018                    if (packet.getType() == IqPacket.TYPE.RESULT) {
2019                        final boolean waitForDisco = enableStreamManagement();
2020                        sendPostBindInitialization(waitForDisco, false);
2021                    } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
2022                        throw new StateChangingError(Account.State.SESSION_FAILURE);
2023                    }
2024                },
2025                true);
2026    }
2027
2028    private boolean enableStreamManagement() {
2029        final boolean streamManagement =
2030                this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT);
2031        if (streamManagement) {
2032            synchronized (this.mStanzaQueue) {
2033                final EnablePacket enable = new EnablePacket();
2034                tagWriter.writeStanzaAsync(enable);
2035                stanzasSent = 0;
2036                mStanzaQueue.clear();
2037            }
2038            return true;
2039        } else {
2040            return false;
2041        }
2042    }
2043
2044    private void sendPostBindInitialization(
2045            final boolean waitForDisco, final boolean carbonsEnabled) {
2046        features.carbonsEnabled = carbonsEnabled;
2047        features.blockListRequested = false;
2048        synchronized (this.disco) {
2049            this.disco.clear();
2050        }
2051        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
2052        mPendingServiceDiscoveries.set(0);
2053        mWaitForDisco.set(waitForDisco);
2054        lastDiscoStarted = SystemClock.elapsedRealtime();
2055        mXmppConnectionService.scheduleWakeUpCall(
2056                Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
2057        final Element caps = streamFeatures.findChild("c");
2058        final String hash = caps == null ? null : caps.getAttribute("hash");
2059        final String ver = caps == null ? null : caps.getAttribute("ver");
2060        ServiceDiscoveryResult discoveryResult = null;
2061        if (hash != null && ver != null) {
2062            discoveryResult =
2063                    mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
2064        }
2065        final boolean requestDiscoItemsFirst =
2066                !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
2067        if (requestDiscoItemsFirst) {
2068            sendServiceDiscoveryItems(account.getDomain());
2069        }
2070        if (discoveryResult == null) {
2071            sendServiceDiscoveryInfo(account.getDomain());
2072        } else {
2073            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache");
2074            disco.put(account.getDomain(), discoveryResult);
2075        }
2076        discoverMamPreferences();
2077        sendServiceDiscoveryInfo(account.getJid().asBareJid());
2078        if (!requestDiscoItemsFirst) {
2079            sendServiceDiscoveryItems(account.getDomain());
2080        }
2081
2082        if (!mWaitForDisco.get()) {
2083            finalizeBind();
2084        }
2085        this.lastSessionStarted = SystemClock.elapsedRealtime();
2086    }
2087
2088    private void sendServiceDiscoveryInfo(final Jid jid) {
2089        mPendingServiceDiscoveries.incrementAndGet();
2090        final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2091        iq.setTo(jid);
2092        iq.query("http://jabber.org/protocol/disco#info");
2093        this.sendIqPacket(
2094                iq,
2095                (account, packet) -> {
2096                    if (packet.getType() == IqPacket.TYPE.RESULT) {
2097                        boolean advancedStreamFeaturesLoaded;
2098                        synchronized (XmppConnection.this.disco) {
2099                            ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
2100                            if (jid.equals(account.getDomain())) {
2101                                mXmppConnectionService.databaseBackend.insertDiscoveryResult(
2102                                        result);
2103                            }
2104                            disco.put(jid, result);
2105                            advancedStreamFeaturesLoaded =
2106                                    disco.containsKey(account.getDomain())
2107                                            && disco.containsKey(account.getJid().asBareJid());
2108                        }
2109                        if (advancedStreamFeaturesLoaded
2110                                && (jid.equals(account.getDomain())
2111                                        || jid.equals(account.getJid().asBareJid()))) {
2112                            enableAdvancedStreamFeatures();
2113                        }
2114                    } else if (packet.getType() == IqPacket.TYPE.ERROR) {
2115                        Log.d(
2116                                Config.LOGTAG,
2117                                account.getJid().asBareJid()
2118                                        + ": could not query disco info for "
2119                                        + jid.toString());
2120                        final boolean serverOrAccount =
2121                                jid.equals(account.getDomain())
2122                                        || jid.equals(account.getJid().asBareJid());
2123                        final boolean advancedStreamFeaturesLoaded;
2124                        if (serverOrAccount) {
2125                            synchronized (XmppConnection.this.disco) {
2126                                disco.put(jid, ServiceDiscoveryResult.empty());
2127                                advancedStreamFeaturesLoaded =
2128                                        disco.containsKey(account.getDomain())
2129                                                && disco.containsKey(account.getJid().asBareJid());
2130                            }
2131                        } else {
2132                            advancedStreamFeaturesLoaded = false;
2133                        }
2134                        if (advancedStreamFeaturesLoaded) {
2135                            enableAdvancedStreamFeatures();
2136                        }
2137                    }
2138                    if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
2139                        if (mPendingServiceDiscoveries.decrementAndGet() == 0
2140                                && mWaitForDisco.compareAndSet(true, false)) {
2141                            finalizeBind();
2142                        }
2143                    }
2144                });
2145    }
2146
2147    private void discoverMamPreferences() {
2148        IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2149        request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
2150        sendIqPacket(
2151                request,
2152                (account, response) -> {
2153                    if (response.getType() == IqPacket.TYPE.RESULT) {
2154                        Element prefs =
2155                                response.findChild(
2156                                        "prefs", MessageArchiveService.Version.MAM_2.namespace);
2157                        isMamPreferenceAlways =
2158                                "always"
2159                                        .equals(
2160                                                prefs == null
2161                                                        ? null
2162                                                        : prefs.getAttribute("default"));
2163                    }
2164                });
2165    }
2166
2167    private void discoverCommands() {
2168        final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2169        request.setTo(account.getDomain());
2170        request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
2171        sendIqPacket(
2172                request,
2173                (account, response) -> {
2174                    if (response.getType() == IqPacket.TYPE.RESULT) {
2175                        final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
2176                        if (query == null) {
2177                            return;
2178                        }
2179                        final HashMap<String, Jid> commands = new HashMap<>();
2180                        for (final Element child : query.getChildren()) {
2181                            if ("item".equals(child.getName())) {
2182                                final String node = child.getAttribute("node");
2183                                final Jid jid = child.getAttributeAsJid("jid");
2184                                if (node != null && jid != null) {
2185                                    commands.put(node, jid);
2186                                }
2187                            }
2188                        }
2189                        synchronized (this.commands) {
2190                            this.commands.clear();
2191                            this.commands.putAll(commands);
2192                        }
2193                    }
2194                });
2195    }
2196
2197    public boolean isMamPreferenceAlways() {
2198        return isMamPreferenceAlways;
2199    }
2200
2201    private void finalizeBind() {
2202        if (bindListener != null) {
2203            bindListener.onBind(account);
2204        }
2205        changeStatusToOnline();
2206    }
2207
2208    private void enableAdvancedStreamFeatures() {
2209        if (getFeatures().blocking() && !features.blockListRequested) {
2210            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
2211            this.sendIqPacket(
2212                    getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
2213        }
2214        for (final OnAdvancedStreamFeaturesLoaded listener :
2215                advancedStreamFeaturesLoadedListeners) {
2216            listener.onAdvancedStreamFeaturesAvailable(account);
2217        }
2218        if (getFeatures().carbons() && !features.carbonsEnabled) {
2219            sendEnableCarbons();
2220        }
2221        if (getFeatures().commands()) {
2222            discoverCommands();
2223        }
2224    }
2225
2226    private void sendServiceDiscoveryItems(final Jid server) {
2227        mPendingServiceDiscoveries.incrementAndGet();
2228        final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2229        iq.setTo(server.getDomain());
2230        iq.query("http://jabber.org/protocol/disco#items");
2231        this.sendIqPacket(
2232                iq,
2233                (account, packet) -> {
2234                    if (packet.getType() == IqPacket.TYPE.RESULT) {
2235                        final HashSet<Jid> items = new HashSet<>();
2236                        final List<Element> elements = packet.query().getChildren();
2237                        for (final Element element : elements) {
2238                            if (element.getName().equals("item")) {
2239                                final Jid jid =
2240                                        InvalidJid.getNullForInvalid(
2241                                                element.getAttributeAsJid("jid"));
2242                                if (jid != null && !jid.equals(account.getDomain())) {
2243                                    items.add(jid);
2244                                }
2245                            }
2246                        }
2247                        for (Jid jid : items) {
2248                            sendServiceDiscoveryInfo(jid);
2249                        }
2250                    } else {
2251                        Log.d(
2252                                Config.LOGTAG,
2253                                account.getJid().asBareJid()
2254                                        + ": could not query disco items of "
2255                                        + server);
2256                    }
2257                    if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
2258                        if (mPendingServiceDiscoveries.decrementAndGet() == 0
2259                                && mWaitForDisco.compareAndSet(true, false)) {
2260                            finalizeBind();
2261                        }
2262                    }
2263                });
2264    }
2265
2266    private void sendEnableCarbons() {
2267        final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2268        iq.addChild("enable", Namespace.CARBONS);
2269        this.sendIqPacket(
2270                iq,
2271                (account, packet) -> {
2272                    if (packet.getType() == IqPacket.TYPE.RESULT) {
2273                        Log.d(
2274                                Config.LOGTAG,
2275                                account.getJid().asBareJid() + ": successfully enabled carbons");
2276                        features.carbonsEnabled = true;
2277                    } else {
2278                        Log.d(
2279                                Config.LOGTAG,
2280                                account.getJid().asBareJid()
2281                                        + ": could not enable carbons "
2282                                        + packet);
2283                    }
2284                });
2285    }
2286
2287    private void processStreamError(final Tag currentTag) throws IOException {
2288        final Element streamError = tagReader.readElement(currentTag);
2289        if (streamError == null) {
2290            return;
2291        }
2292        if (streamError.hasChild("conflict")) {
2293            account.setResource(createNewResource());
2294            Log.d(
2295                    Config.LOGTAG,
2296                    account.getJid().asBareJid()
2297                            + ": switching resource due to conflict ("
2298                            + account.getResource()
2299                            + ")");
2300            throw new IOException();
2301        } else if (streamError.hasChild("host-unknown")) {
2302            throw new StateChangingException(Account.State.HOST_UNKNOWN);
2303        } else if (streamError.hasChild("policy-violation")) {
2304            this.lastConnect = SystemClock.elapsedRealtime();
2305            final String text = streamError.findChildContent("text");
2306            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
2307            failPendingMessages(text);
2308            throw new StateChangingException(Account.State.POLICY_VIOLATION);
2309        } else if (streamError.hasChild("see-other-host")) {
2310            final String seeOtherHost = streamError.findChildContent("see-other-host");
2311            final Resolver.Result currentResolverResult = this.currentResolverResult;
2312            if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
2313                Log.d(
2314                        Config.LOGTAG,
2315                        account.getJid().asBareJid() + ": stream error " + streamError);
2316                throw new StateChangingException(Account.State.STREAM_ERROR);
2317            }
2318            Log.d(
2319                    Config.LOGTAG,
2320                    account.getJid().asBareJid()
2321                            + ": see other host: "
2322                            + seeOtherHost
2323                            + " "
2324                            + currentResolverResult);
2325            final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
2326            if (seeOtherResult != null) {
2327                this.seeOtherHostResolverResult = seeOtherResult;
2328                throw new StateChangingException(Account.State.SEE_OTHER_HOST);
2329            } else {
2330                throw new StateChangingException(Account.State.STREAM_ERROR);
2331            }
2332        } else {
2333            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2334            throw new StateChangingException(Account.State.STREAM_ERROR);
2335        }
2336    }
2337
2338    private void failPendingMessages(final String error) {
2339        synchronized (this.mStanzaQueue) {
2340            for (int i = 0; i < mStanzaQueue.size(); ++i) {
2341                final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
2342                if (stanza instanceof MessagePacket packet) {
2343                    final String id = packet.getId();
2344                    final Jid to = packet.getTo();
2345                    mXmppConnectionService.markMessage(
2346                            account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error);
2347                }
2348            }
2349        }
2350    }
2351
2352    private boolean establishStream(final SSLSockets.Version sslVersion)
2353            throws IOException, InterruptedException {
2354        final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
2355        final SaslMechanism quickStartMechanism;
2356        if (secureConnection) {
2357            quickStartMechanism =
2358                    SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
2359        } else {
2360            quickStartMechanism = null;
2361        }
2362        if (secureConnection
2363                && Config.QUICKSTART_ENABLED
2364                && quickStartMechanism != null
2365                && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
2366            mXmppConnectionService.restoredFromDatabaseLatch.await();
2367            this.loginInfo =
2368                    new LoginInfo(
2369                            quickStartMechanism,
2370                            SaslMechanism.Version.SASL_2,
2371                            Bind2.QUICKSTART_FEATURES);
2372            final boolean usingFast = quickStartMechanism instanceof HashedToken;
2373            final Element authenticate =
2374                    generateAuthenticationRequest(
2375                            quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)),
2376                            usingFast);
2377            authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism());
2378            sendStartStream(true, false);
2379            synchronized (this.mStanzaQueue) {
2380                this.stanzasSentBeforeAuthentication = this.stanzasSent;
2381                tagWriter.writeElement(authenticate);
2382            }
2383            Log.d(
2384                    Config.LOGTAG,
2385                    account.getJid().toString()
2386                            + ": quick start with "
2387                            + quickStartMechanism.getMechanism());
2388            return true;
2389        } else {
2390            sendStartStream(secureConnection, true);
2391            return false;
2392        }
2393    }
2394
2395    private void sendStartStream(final boolean from, final boolean flush) throws IOException {
2396        final Tag stream = Tag.start("stream:stream");
2397        stream.setAttribute("to", account.getServer());
2398        if (from) {
2399            stream.setAttribute("from", account.getJid().asBareJid().toEscapedString());
2400        }
2401        stream.setAttribute("version", "1.0");
2402        stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
2403        stream.setAttribute("xmlns", Namespace.JABBER_CLIENT);
2404        stream.setAttribute("xmlns:stream", Namespace.STREAMS);
2405        tagWriter.writeTag(stream, flush);
2406    }
2407
2408    private String createNewResource() {
2409        return mXmppConnectionService.getString(R.string.app_name) + '.' + nextRandomId(true);
2410    }
2411
2412    private String nextRandomId() {
2413        return nextRandomId(false);
2414    }
2415
2416    private String nextRandomId(final boolean s) {
2417        return CryptoHelper.random(s ? 3 : 9);
2418    }
2419
2420    public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
2421        packet.setFrom(account.getJid());
2422        return this.sendUnmodifiedIqPacket(packet, callback, false);
2423    }
2424
2425    public synchronized String sendUnmodifiedIqPacket(
2426            final IqPacket packet, final OnIqPacketReceived callback, boolean force) {
2427        if (packet.getId() == null) {
2428            packet.setAttribute("id", nextRandomId());
2429        }
2430        if (callback != null) {
2431            synchronized (this.packetCallbacks) {
2432                packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
2433            }
2434        }
2435        this.sendPacket(packet, force);
2436        return packet.getId();
2437    }
2438
2439    public void sendMessagePacket(final MessagePacket packet) {
2440        this.sendPacket(packet);
2441    }
2442
2443    public void sendPresencePacket(final PresencePacket packet) {
2444        this.sendPacket(packet);
2445    }
2446
2447    private synchronized void sendPacket(final AbstractStanza packet) {
2448        sendPacket(packet, false);
2449    }
2450
2451    private synchronized void sendPacket(final AbstractStanza packet, final boolean force) {
2452        if (stanzasSent == Integer.MAX_VALUE) {
2453            resetStreamId();
2454            disconnect(true);
2455            return;
2456        }
2457        synchronized (this.mStanzaQueue) {
2458            if (force || isBound) {
2459                tagWriter.writeStanzaAsync(packet);
2460            } else {
2461                Log.d(
2462                        Config.LOGTAG,
2463                        account.getJid().asBareJid()
2464                                + " do not write stanza to unbound stream "
2465                                + packet.toString());
2466            }
2467            if (packet instanceof AbstractAcknowledgeableStanza stanza) {
2468                if (this.mStanzaQueue.size() != 0) {
2469                    int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2470                    if (currentHighestKey != stanzasSent) {
2471                        throw new AssertionError("Stanza count messed up");
2472                    }
2473                }
2474
2475                ++stanzasSent;
2476                if (Config.EXTENDED_SM_LOGGING) {
2477                    Log.d(
2478                            Config.LOGTAG,
2479                            account.getJid().asBareJid()
2480                                    + ": counting outbound "
2481                                    + packet.getName()
2482                                    + " as #"
2483                                    + stanzasSent);
2484                }
2485                this.mStanzaQueue.append(stanzasSent, stanza);
2486                if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
2487                    if (Config.EXTENDED_SM_LOGGING) {
2488                        Log.d(
2489                                Config.LOGTAG,
2490                                account.getJid().asBareJid()
2491                                        + ": requesting ack for message stanza #"
2492                                        + stanzasSent);
2493                    }
2494                    tagWriter.writeStanzaAsync(new RequestPacket());
2495                }
2496            }
2497        }
2498    }
2499
2500    public void sendPing() {
2501        if (!r()) {
2502            final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2503            iq.setFrom(account.getJid());
2504            iq.addChild("ping", Namespace.PING);
2505            this.sendIqPacket(iq, null);
2506        }
2507        this.lastPingSent = SystemClock.elapsedRealtime();
2508    }
2509
2510    public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) {
2511        this.messageListener = listener;
2512    }
2513
2514    public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) {
2515        this.unregisteredIqListener = listener;
2516    }
2517
2518    public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) {
2519        this.presenceListener = listener;
2520    }
2521
2522    public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2523        this.jingleListener = listener;
2524    }
2525
2526    public void setOnStatusChangedListener(final OnStatusChanged listener) {
2527        this.statusListener = listener;
2528    }
2529
2530    public void setOnBindListener(final OnBindListener listener) {
2531        this.bindListener = listener;
2532    }
2533
2534    public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2535        this.acknowledgedListener = listener;
2536    }
2537
2538    public void addOnAdvancedStreamFeaturesAvailableListener(
2539            final OnAdvancedStreamFeaturesLoaded listener) {
2540        this.advancedStreamFeaturesLoadedListeners.add(listener);
2541    }
2542
2543    private void forceCloseSocket() {
2544        FileBackend.close(this.socket);
2545        FileBackend.close(this.tagReader);
2546    }
2547
2548    public void interrupt() {
2549        if (this.mThread != null) {
2550            this.mThread.interrupt();
2551        }
2552    }
2553
2554    public void disconnect(final boolean force) {
2555        interrupt();
2556        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2557        if (force) {
2558            forceCloseSocket();
2559        } else {
2560            final TagWriter currentTagWriter = this.tagWriter;
2561            if (currentTagWriter.isActive()) {
2562                currentTagWriter.finish();
2563                final Socket currentSocket = this.socket;
2564                final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2565                try {
2566                    currentTagWriter.await(1, TimeUnit.SECONDS);
2567                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2568                    currentTagWriter.writeTag(Tag.end("stream:stream"));
2569                    if (streamCountDownLatch != null) {
2570                        if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2571                            Log.d(
2572                                    Config.LOGTAG,
2573                                    account.getJid().asBareJid() + ": remote ended stream");
2574                        } else {
2575                            Log.d(
2576                                    Config.LOGTAG,
2577                                    account.getJid().asBareJid()
2578                                            + ": remote has not closed socket. force closing");
2579                        }
2580                    }
2581                } catch (InterruptedException e) {
2582                    Log.d(
2583                            Config.LOGTAG,
2584                            account.getJid().asBareJid()
2585                                    + ": interrupted while gracefully closing stream");
2586                } catch (final IOException e) {
2587                    Log.d(
2588                            Config.LOGTAG,
2589                            account.getJid().asBareJid()
2590                                    + ": io exception during disconnect ("
2591                                    + e.getMessage()
2592                                    + ")");
2593                } finally {
2594                    FileBackend.close(currentSocket);
2595                }
2596            } else {
2597                forceCloseSocket();
2598            }
2599        }
2600    }
2601
2602    private void resetStreamId() {
2603        this.streamId = null;
2604        this.boundStreamFeatures = null;
2605    }
2606
2607    private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
2608        synchronized (this.disco) {
2609            final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
2610            for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
2611                if (cursor.getValue().getFeatures().contains(feature)) {
2612                    items.add(cursor);
2613                }
2614            }
2615            return items;
2616        }
2617    }
2618
2619    public Jid findDiscoItemByFeature(final String feature) {
2620        final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
2621        if (items.size() >= 1) {
2622            return items.get(0).getKey();
2623        }
2624        return null;
2625    }
2626
2627    public boolean r() {
2628        if (getFeatures().sm()) {
2629            this.tagWriter.writeStanzaAsync(new RequestPacket());
2630            return true;
2631        } else {
2632            return false;
2633        }
2634    }
2635
2636    public List<String> getMucServersWithholdAccount() {
2637        final List<String> servers = getMucServers();
2638        servers.remove(account.getDomain().toEscapedString());
2639        return servers;
2640    }
2641
2642    public List<String> getMucServers() {
2643        List<String> servers = new ArrayList<>();
2644        synchronized (this.disco) {
2645            for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
2646                final ServiceDiscoveryResult value = cursor.getValue();
2647                if (value.getFeatures().contains("http://jabber.org/protocol/muc")
2648                        && value.hasIdentity("conference", "text")
2649                        && !value.getFeatures().contains("jabber:iq:gateway")
2650                        && !value.hasIdentity("conference", "irc")) {
2651                    servers.add(cursor.getKey().toString());
2652                }
2653            }
2654        }
2655        return servers;
2656    }
2657
2658    public String getMucServer() {
2659        List<String> servers = getMucServers();
2660        return servers.size() > 0 ? servers.get(0) : null;
2661    }
2662
2663    public int getTimeToNextAttempt(final boolean aggressive) {
2664        final int interval;
2665        if (aggressive) {
2666            interval = Math.min((int) (3 * Math.pow(1.3, attempt)), 60);
2667        } else {
2668            final int additionalTime =
2669                    account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2670            interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2671        }
2672        final int secondsSinceLast =
2673                (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
2674        return interval - secondsSinceLast;
2675    }
2676
2677    public int getAttempt() {
2678        return this.attempt;
2679    }
2680
2681    public Features getFeatures() {
2682        return this.features;
2683    }
2684
2685    public long getLastSessionEstablished() {
2686        final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2687        return System.currentTimeMillis() - diff;
2688    }
2689
2690    public long getLastConnect() {
2691        return this.lastConnect;
2692    }
2693
2694    public long getLastPingSent() {
2695        return this.lastPingSent;
2696    }
2697
2698    public long getLastDiscoStarted() {
2699        return this.lastDiscoStarted;
2700    }
2701
2702    public long getLastPacketReceived() {
2703        return this.lastPacketReceived;
2704    }
2705
2706    public void sendActive() {
2707        this.sendPacket(new ActivePacket());
2708    }
2709
2710    public void sendInactive() {
2711        this.sendPacket(new InactivePacket());
2712    }
2713
2714    public void resetAttemptCount(boolean resetConnectTime) {
2715        this.attempt = 0;
2716        if (resetConnectTime) {
2717            this.lastConnect = 0;
2718        }
2719    }
2720
2721    public void setInteractive(boolean interactive) {
2722        this.mInteractive = interactive;
2723    }
2724
2725    private IqGenerator getIqGenerator() {
2726        return mXmppConnectionService.getIqGenerator();
2727    }
2728
2729    private class MyKeyManager implements X509KeyManager {
2730        @Override
2731        public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2732            return account.getPrivateKeyAlias();
2733        }
2734
2735        @Override
2736        public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
2737            return null;
2738        }
2739
2740        @Override
2741        public X509Certificate[] getCertificateChain(String alias) {
2742            Log.d(Config.LOGTAG, "getting certificate chain");
2743            try {
2744                return KeyChain.getCertificateChain(mXmppConnectionService, alias);
2745            } catch (final Exception e) {
2746                Log.d(Config.LOGTAG, "could not get certificate chain", e);
2747                return new X509Certificate[0];
2748            }
2749        }
2750
2751        @Override
2752        public String[] getClientAliases(String s, Principal[] principals) {
2753            final String alias = account.getPrivateKeyAlias();
2754            return alias != null ? new String[] {alias} : new String[0];
2755        }
2756
2757        @Override
2758        public String[] getServerAliases(String s, Principal[] principals) {
2759            return new String[0];
2760        }
2761
2762        @Override
2763        public PrivateKey getPrivateKey(String alias) {
2764            try {
2765                return KeyChain.getPrivateKey(mXmppConnectionService, alias);
2766            } catch (Exception e) {
2767                return null;
2768            }
2769        }
2770    }
2771
2772    private static class LoginInfo {
2773        public final SaslMechanism saslMechanism;
2774        public final SaslMechanism.Version saslVersion;
2775        public final List<String> inlineBindFeatures;
2776        public final AtomicBoolean success = new AtomicBoolean(false);
2777
2778        private LoginInfo(
2779                final SaslMechanism saslMechanism,
2780                final SaslMechanism.Version saslVersion,
2781                final Collection<String> inlineBindFeatures) {
2782            Preconditions.checkNotNull(saslMechanism, "SASL Mechanism must not be null");
2783            Preconditions.checkNotNull(saslVersion, "SASL version must not be null");
2784            this.saslMechanism = saslMechanism;
2785            this.saslVersion = saslVersion;
2786            this.inlineBindFeatures =
2787                    inlineBindFeatures == null
2788                            ? Collections.emptyList()
2789                            : ImmutableList.copyOf(inlineBindFeatures);
2790        }
2791
2792        public static SaslMechanism mechanism(final LoginInfo loginInfo) {
2793            return loginInfo == null ? null : loginInfo.saslMechanism;
2794        }
2795
2796        public void success(final String challenge, final SSLSocket sslSocket)
2797                throws SaslMechanism.AuthenticationException {
2798            final var response = this.saslMechanism.getResponse(challenge, sslSocket);
2799            if (!Strings.isNullOrEmpty(response)) {
2800                throw new SaslMechanism.AuthenticationException(
2801                        "processing success yielded another response");
2802            }
2803            if (this.success.compareAndSet(false, true)) {
2804                return;
2805            }
2806            throw new SaslMechanism.AuthenticationException("Process 'success' twice");
2807        }
2808
2809        public static boolean isSuccess(final LoginInfo loginInfo) {
2810            return loginInfo != null && loginInfo.success.get();
2811        }
2812    }
2813
2814    private static class StreamId {
2815        public final String id;
2816        public final Resolver.Result location;
2817
2818        private StreamId(String id, Resolver.Result location) {
2819            this.id = id;
2820            this.location = location;
2821        }
2822
2823        @NonNull
2824        @Override
2825        public String toString() {
2826            return MoreObjects.toStringHelper(this)
2827                    .add("id", id)
2828                    .add("location", location)
2829                    .toString();
2830        }
2831    }
2832
2833    private static class StateChangingError extends Error {
2834        private final Account.State state;
2835
2836        public StateChangingError(Account.State state) {
2837            this.state = state;
2838        }
2839    }
2840
2841    private static class StateChangingException extends IOException {
2842        private final Account.State state;
2843
2844        public StateChangingException(Account.State state) {
2845            this.state = state;
2846        }
2847    }
2848
2849    public class Features {
2850        XmppConnection connection;
2851        private boolean carbonsEnabled = false;
2852        private boolean encryptionEnabled = false;
2853        private boolean blockListRequested = false;
2854
2855        public Features(final XmppConnection connection) {
2856            this.connection = connection;
2857        }
2858
2859        private boolean hasDiscoFeature(final Jid server, final String feature) {
2860            synchronized (XmppConnection.this.disco) {
2861                final ServiceDiscoveryResult sdr = connection.disco.get(server);
2862                return sdr != null && sdr.getFeatures().contains(feature);
2863            }
2864        }
2865
2866        public boolean carbons() {
2867            return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
2868        }
2869
2870        public boolean commands() {
2871            return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
2872        }
2873
2874        public boolean easyOnboardingInvites() {
2875            synchronized (commands) {
2876                return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
2877            }
2878        }
2879
2880        public boolean bookmarksConversion() {
2881            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
2882                    && pepPublishOptions();
2883        }
2884
2885        public boolean blocking() {
2886            return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
2887        }
2888
2889        public boolean spamReporting() {
2890            return hasDiscoFeature(account.getDomain(), Namespace.REPORTING);
2891        }
2892
2893        public boolean flexibleOfflineMessageRetrieval() {
2894            return hasDiscoFeature(
2895                    account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
2896        }
2897
2898        public boolean register() {
2899            return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
2900        }
2901
2902        public boolean invite() {
2903            return connection.streamFeatures != null
2904                    && connection.streamFeatures.hasChild("register", Namespace.INVITE);
2905        }
2906
2907        public boolean sm() {
2908            return streamId != null
2909                    || (connection.streamFeatures != null
2910                            && connection.streamFeatures.hasChild(
2911                                    "sm", Namespace.STREAM_MANAGEMENT));
2912        }
2913
2914        public boolean csi() {
2915            return connection.streamFeatures != null
2916                    && connection.streamFeatures.hasChild("csi", Namespace.CSI);
2917        }
2918
2919        public boolean pep() {
2920            synchronized (XmppConnection.this.disco) {
2921                ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2922                return info != null && info.hasIdentity("pubsub", "pep");
2923            }
2924        }
2925
2926        public boolean pepPersistent() {
2927            synchronized (XmppConnection.this.disco) {
2928                ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2929                return info != null
2930                        && info.getFeatures()
2931                                .contains("http://jabber.org/protocol/pubsub#persistent-items");
2932            }
2933        }
2934
2935        public boolean pepPublishOptions() {
2936            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
2937        }
2938
2939        public boolean pepOmemoWhitelisted() {
2940            return hasDiscoFeature(
2941                    account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
2942        }
2943
2944        public boolean mam() {
2945            return MessageArchiveService.Version.has(getAccountFeatures());
2946        }
2947
2948        public List<String> getAccountFeatures() {
2949            ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
2950            return result == null ? Collections.emptyList() : result.getFeatures();
2951        }
2952
2953        public boolean push() {
2954            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
2955                    || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
2956        }
2957
2958        public boolean rosterVersioning() {
2959            return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
2960        }
2961
2962        public void setBlockListRequested(boolean value) {
2963            this.blockListRequested = value;
2964        }
2965
2966        public boolean httpUpload(long filesize) {
2967            if (Config.DISABLE_HTTP_UPLOAD) {
2968                return false;
2969            } else {
2970                for (String namespace :
2971                        new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2972                    List<Entry<Jid, ServiceDiscoveryResult>> items =
2973                            findDiscoItemsByFeature(namespace);
2974                    if (items.size() > 0) {
2975                        try {
2976                            long maxsize =
2977                                    Long.parseLong(
2978                                            items.get(0)
2979                                                    .getValue()
2980                                                    .getExtendedDiscoInformation(
2981                                                            namespace, "max-file-size"));
2982                            if (filesize <= maxsize) {
2983                                return true;
2984                            } else {
2985                                Log.d(
2986                                        Config.LOGTAG,
2987                                        account.getJid().asBareJid()
2988                                                + ": http upload is not available for files with size "
2989                                                + filesize
2990                                                + " (max is "
2991                                                + maxsize
2992                                                + ")");
2993                                return false;
2994                            }
2995                        } catch (Exception e) {
2996                            return true;
2997                        }
2998                    }
2999                }
3000                return false;
3001            }
3002        }
3003
3004        public boolean useLegacyHttpUpload() {
3005            return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null
3006                    && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
3007        }
3008
3009        public long getMaxHttpUploadSize() {
3010            for (String namespace :
3011                    new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
3012                List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
3013                if (items.size() > 0) {
3014                    try {
3015                        return Long.parseLong(
3016                                items.get(0)
3017                                        .getValue()
3018                                        .getExtendedDiscoInformation(namespace, "max-file-size"));
3019                    } catch (Exception e) {
3020                        // ignored
3021                    }
3022                }
3023            }
3024            return -1;
3025        }
3026
3027        public boolean stanzaIds() {
3028            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
3029        }
3030
3031        public boolean bookmarks2() {
3032            return pepPublishOptions()
3033                    && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
3034        }
3035
3036        public boolean externalServiceDiscovery() {
3037            return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
3038        }
3039    }
3040}