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