XmppConnection.java

   1package eu.siacs.conversations.xmpp;
   2
   3import android.content.Context;
   4import android.graphics.Bitmap;
   5import android.graphics.BitmapFactory;
   6import android.os.SystemClock;
   7import android.security.KeyChain;
   8import android.util.Base64;
   9import android.util.Log;
  10import android.util.Pair;
  11import android.util.SparseArray;
  12
  13import org.xmlpull.v1.XmlPullParserException;
  14
  15import java.io.ByteArrayInputStream;
  16import java.io.IOException;
  17import java.io.InputStream;
  18import java.net.ConnectException;
  19import java.net.IDN;
  20import java.net.InetAddress;
  21import java.net.InetSocketAddress;
  22import java.net.MalformedURLException;
  23import java.net.Socket;
  24import java.net.URL;
  25import java.net.UnknownHostException;
  26import java.security.KeyManagementException;
  27import java.security.NoSuchAlgorithmException;
  28import java.security.Principal;
  29import java.security.PrivateKey;
  30import java.security.cert.X509Certificate;
  31import java.util.ArrayList;
  32import java.util.Arrays;
  33import java.util.HashMap;
  34import java.util.HashSet;
  35import java.util.Hashtable;
  36import java.util.Iterator;
  37import java.util.List;
  38import java.util.Map.Entry;
  39import java.util.concurrent.CountDownLatch;
  40import java.util.concurrent.TimeUnit;
  41import java.util.concurrent.atomic.AtomicBoolean;
  42import java.util.concurrent.atomic.AtomicInteger;
  43import java.util.regex.Matcher;
  44
  45import javax.net.ssl.KeyManager;
  46import javax.net.ssl.SSLContext;
  47import javax.net.ssl.SSLSession;
  48import javax.net.ssl.SSLSocket;
  49import javax.net.ssl.SSLSocketFactory;
  50import javax.net.ssl.X509KeyManager;
  51import javax.net.ssl.X509TrustManager;
  52
  53import eu.siacs.conversations.Config;
  54import eu.siacs.conversations.R;
  55import eu.siacs.conversations.crypto.DomainHostnameVerifier;
  56import eu.siacs.conversations.crypto.XmppDomainVerifier;
  57import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  58import eu.siacs.conversations.crypto.sasl.Anonymous;
  59import eu.siacs.conversations.crypto.sasl.DigestMd5;
  60import eu.siacs.conversations.crypto.sasl.External;
  61import eu.siacs.conversations.crypto.sasl.Plain;
  62import eu.siacs.conversations.crypto.sasl.SaslMechanism;
  63import eu.siacs.conversations.crypto.sasl.ScramSha1;
  64import eu.siacs.conversations.crypto.sasl.ScramSha256;
  65import eu.siacs.conversations.entities.Account;
  66import eu.siacs.conversations.entities.Message;
  67import eu.siacs.conversations.entities.ServiceDiscoveryResult;
  68import eu.siacs.conversations.generator.IqGenerator;
  69import eu.siacs.conversations.persistance.FileBackend;
  70import eu.siacs.conversations.services.MemorizingTrustManager;
  71import eu.siacs.conversations.services.NotificationService;
  72import eu.siacs.conversations.services.XmppConnectionService;
  73import eu.siacs.conversations.utils.CryptoHelper;
  74import eu.siacs.conversations.utils.IP;
  75import eu.siacs.conversations.utils.Patterns;
  76import eu.siacs.conversations.utils.Resolver;
  77import eu.siacs.conversations.utils.SSLSocketHelper;
  78import eu.siacs.conversations.utils.SocksSocketFactory;
  79import eu.siacs.conversations.xml.Element;
  80import eu.siacs.conversations.xml.Tag;
  81import eu.siacs.conversations.xml.TagWriter;
  82import eu.siacs.conversations.xml.XmlReader;
  83import eu.siacs.conversations.xml.Namespace;
  84import eu.siacs.conversations.xmpp.forms.Data;
  85import eu.siacs.conversations.xmpp.forms.Field;
  86import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  87import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  88import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza;
  89import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
  90import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  91import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  92import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  93import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
  94import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
  95import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
  96import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
  97import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
  98import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
  99import rocks.xmpp.addr.Jid;
 100
 101public class XmppConnection implements Runnable {
 102
 103	private static final int PACKET_IQ = 0;
 104	private static final int PACKET_MESSAGE = 1;
 105	private static final int PACKET_PRESENCE = 2;
 106	public final OnIqPacketReceived registrationResponseListener = new OnIqPacketReceived() {
 107		@Override
 108		public void onIqPacketReceived(Account account, IqPacket packet) {
 109			if (packet.getType() == IqPacket.TYPE.RESULT) {
 110				account.setOption(Account.OPTION_REGISTER, false);
 111				throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
 112			} else {
 113				final List<String> PASSWORD_TOO_WEAK_MSGS = Arrays.asList(
 114						"The password is too weak",
 115						"Please use a longer password.");
 116				Element error = packet.findChild("error");
 117				Account.State state = Account.State.REGISTRATION_FAILED;
 118				if (error != null) {
 119					if (error.hasChild("conflict")) {
 120						state = Account.State.REGISTRATION_CONFLICT;
 121					} else if (error.hasChild("resource-constraint")
 122							&& "wait".equals(error.getAttribute("type"))) {
 123						state = Account.State.REGISTRATION_PLEASE_WAIT;
 124					} else if (error.hasChild("not-acceptable")
 125							&& PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) {
 126						state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
 127					}
 128				}
 129				throw new StateChangingError(state);
 130			}
 131		}
 132	};
 133	protected final Account account;
 134	private final Features features = new Features(this);
 135	private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
 136	private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
 137	private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks = new Hashtable<>();
 138	private final ArrayList<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners = new ArrayList<>();
 139	private final XmppConnectionService mXmppConnectionService;
 140	private Socket socket;
 141	private XmlReader tagReader;
 142	private TagWriter tagWriter = new TagWriter();
 143	private boolean shouldAuthenticate = true;
 144	private boolean inSmacksSession = false;
 145	private boolean isBound = false;
 146	private Element streamFeatures;
 147	private String streamId = null;
 148	private int smVersion = 3;
 149	private int stanzasReceived = 0;
 150	private int stanzasSent = 0;
 151	private long lastPacketReceived = 0;
 152	private long lastPingSent = 0;
 153	private long lastConnect = 0;
 154	private long lastSessionStarted = 0;
 155	private long lastDiscoStarted = 0;
 156	private AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
 157	private AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
 158	private AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
 159	private AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
 160	private boolean mInteractive = false;
 161	private int attempt = 0;
 162	private OnPresencePacketReceived presenceListener = null;
 163	private OnJinglePacketReceived jingleListener = null;
 164	private OnIqPacketReceived unregisteredIqListener = null;
 165	private OnMessagePacketReceived messageListener = null;
 166	private OnStatusChanged statusListener = null;
 167	private OnBindListener bindListener = null;
 168	private OnMessageAcknowledged acknowledgedListener = null;
 169	private SaslMechanism saslMechanism;
 170	private URL redirectionUrl = null;
 171	private String verifiedHostname = null;
 172	private Thread mThread;
 173	private CountDownLatch mStreamCountDownLatch;
 174
 175	public XmppConnection(final Account account, final XmppConnectionService service) {
 176		this.account = account;
 177		final String tag = account.getJid().asBareJid().toString();
 178		mXmppConnectionService = service;
 179	}
 180
 181	protected void changeStatus(final Account.State nextStatus) {
 182		synchronized (this) {
 183			if (Thread.currentThread().isInterrupted()) {
 184				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": not changing status to " + nextStatus + " because thread was interrupted");
 185				return;
 186			}
 187			if (account.getStatus() != nextStatus) {
 188				if ((nextStatus == Account.State.OFFLINE)
 189						&& (account.getStatus() != Account.State.CONNECTING)
 190						&& (account.getStatus() != Account.State.ONLINE)
 191						&& (account.getStatus() != Account.State.DISABLED)) {
 192					return;
 193				}
 194				if (nextStatus == Account.State.ONLINE) {
 195					this.attempt = 0;
 196				}
 197				account.setStatus(nextStatus);
 198			} else {
 199				return;
 200			}
 201		}
 202		if (statusListener != null) {
 203			statusListener.onStatusChanged(account);
 204		}
 205	}
 206
 207	public void prepareNewConnection() {
 208		this.lastConnect = SystemClock.elapsedRealtime();
 209		this.lastPingSent = SystemClock.elapsedRealtime();
 210		this.lastDiscoStarted = Long.MAX_VALUE;
 211		this.mWaitingForSmCatchup.set(false);
 212		this.changeStatus(Account.State.CONNECTING);
 213	}
 214
 215	public boolean isWaitingForSmCatchup() {
 216		return mWaitingForSmCatchup.get();
 217	}
 218
 219	public void incrementSmCatchupMessageCounter() {
 220		this.mSmCatchupMessageCounter.incrementAndGet();
 221	}
 222
 223	protected void connect() {
 224		if (mXmppConnectionService.areMessagesInitialized()) {
 225			mXmppConnectionService.resetSendingToWaiting(account);
 226		}
 227		Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting");
 228		features.encryptionEnabled = false;
 229		inSmacksSession = false;
 230		isBound = false;
 231		this.attempt++;
 232		this.verifiedHostname = null; //will be set if user entered hostname is being used or hostname was verified with dnssec
 233		try {
 234			Socket localSocket;
 235			shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER);
 236			this.changeStatus(Account.State.CONNECTING);
 237			final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion();
 238			final boolean extended = mXmppConnectionService.showExtendedConnectionOptions();
 239			if (useTor) {
 240				String destination;
 241				if (account.getHostname().isEmpty()) {
 242					destination = account.getServer().toString();
 243				} else {
 244					destination = account.getHostname();
 245					this.verifiedHostname = destination;
 246				}
 247				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor");
 248				localSocket = SocksSocketFactory.createSocketOverTor(destination, account.getPort());
 249				try {
 250					startXmpp(localSocket);
 251				} catch (InterruptedException e) {
 252					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream");
 253					return;
 254				} catch (Exception e) {
 255					throw new IOException(e.getMessage());
 256				}
 257			} else if (extended && !account.getHostname().isEmpty()) {
 258
 259				this.verifiedHostname = account.getHostname();
 260
 261				try {
 262					InetSocketAddress address = new InetSocketAddress(this.verifiedHostname, account.getPort());
 263					features.encryptionEnabled = address.getPort() == 5223;
 264					if (features.encryptionEnabled) {
 265						try {
 266							final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
 267							localSocket = tlsFactoryVerifier.factory.createSocket();
 268							localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000);
 269							final SSLSession session = ((SSLSocket) localSocket).getSession();
 270							final String domain = account.getJid().getDomain();
 271							if (!tlsFactoryVerifier.verifier.verify(domain, this.verifiedHostname, session)) {
 272								Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
 273								throw new StateChangingException(Account.State.TLS_ERROR);
 274							}
 275						} catch (KeyManagementException e) {
 276							throw new StateChangingException(Account.State.TLS_ERROR);
 277						}
 278					} else {
 279						localSocket = new Socket();
 280						localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000);
 281					}
 282				} catch (IOException | IllegalArgumentException e) {
 283					throw new UnknownHostException();
 284				}
 285				try {
 286					startXmpp(localSocket);
 287				} catch (InterruptedException e) {
 288					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream");
 289					return;
 290				} catch (Exception e) {
 291					throw new IOException(e.getMessage());
 292				}
 293			} else if (IP.matches(account.getServer())) {
 294				localSocket = new Socket();
 295				try {
 296					localSocket.connect(new InetSocketAddress(account.getServer(), 5222), Config.SOCKET_TIMEOUT * 1000);
 297				} catch (IOException e) {
 298					throw new UnknownHostException();
 299				}
 300				try {
 301					startXmpp(localSocket);
 302				} catch (InterruptedException e) {
 303					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream");
 304					return;
 305				} catch (Exception e) {
 306					throw new IOException(e.getMessage());
 307				}
 308			} else {
 309				final String domain = account.getJid().getDomain();
 310				List<Resolver.Result> results = Resolver.resolve(account.getJid().getDomain());
 311				Resolver.Result storedBackupResult;
 312				if (!Thread.currentThread().isInterrupted()) {
 313					storedBackupResult = mXmppConnectionService.databaseBackend.findResolverResult(domain);
 314					if (storedBackupResult != null && !results.contains(storedBackupResult)) {
 315						results.add(storedBackupResult);
 316						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": loaded backup resolver result from db: " + storedBackupResult);
 317					}
 318				} else {
 319					storedBackupResult = null;
 320				}
 321				for (Iterator<Resolver.Result> iterator = results.iterator(); iterator.hasNext(); ) {
 322					final Resolver.Result result = iterator.next();
 323					if (Thread.currentThread().isInterrupted()) {
 324						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted");
 325						return;
 326					}
 327					try {
 328						// if tls is true, encryption is implied and must not be started
 329						features.encryptionEnabled = result.isDirectTls();
 330						verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null;
 331						final InetSocketAddress addr;
 332						if (result.getIp() != null) {
 333							addr = new InetSocketAddress(result.getIp(), result.getPort());
 334							Log.d(Config.LOGTAG, account.getJid().asBareJid().toString()
 335									+ ": using values from dns " + result.getHostname().toString()
 336									+ "/" + result.getIp().getHostAddress() + ":" + result.getPort() + " tls: " + features.encryptionEnabled);
 337						} else {
 338							addr = new InetSocketAddress(IDN.toASCII(result.getHostname().toString()), result.getPort());
 339							Log.d(Config.LOGTAG, account.getJid().asBareJid().toString()
 340									+ ": using values from dns "
 341									+ result.getHostname().toString() + ":" + result.getPort() + " tls: " + features.encryptionEnabled);
 342						}
 343
 344						if (!features.encryptionEnabled) {
 345							localSocket = new Socket();
 346							localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
 347						} else {
 348							final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
 349							localSocket = tlsFactoryVerifier.factory.createSocket();
 350
 351							if (localSocket == null) {
 352								throw new IOException("could not initialize ssl socket");
 353							}
 354
 355							SSLSocketHelper.setSecurity((SSLSocket) localSocket);
 356							SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) localSocket, account.getServer());
 357							SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) localSocket, "xmpp-client");
 358
 359							localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
 360
 361							if (!tlsFactoryVerifier.verifier.verify(account.getServer(), verifiedHostname, ((SSLSocket) localSocket).getSession())) {
 362								Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
 363								if (!iterator.hasNext()) {
 364									throw new StateChangingException(Account.State.TLS_ERROR);
 365								}
 366							}
 367						}
 368						if (startXmpp(localSocket)) {
 369							if (!result.equals(storedBackupResult)) {
 370								mXmppConnectionService.databaseBackend.saveResolverResult(domain, result);
 371							}
 372							break; // successfully connected to server that speaks xmpp
 373						} else {
 374							localSocket.close();
 375						}
 376					} catch (final StateChangingException e) {
 377						throw e;
 378					} catch (InterruptedException e) {
 379						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream");
 380						return;
 381					} catch (final Throwable e) {
 382						Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage() + "(" + e.getClass().getName() + ")");
 383						if (!iterator.hasNext()) {
 384							throw new UnknownHostException();
 385						}
 386					}
 387				}
 388			}
 389			processStream();
 390		} catch (final SecurityException e) {
 391			this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
 392		} catch (final StateChangingException e) {
 393			this.changeStatus(e.state);
 394		} catch (final Resolver.NetworkIsUnreachableException e) {
 395			this.changeStatus(Account.State.NETWORK_IS_UNREACHABLE);
 396		} catch (final UnknownHostException | ConnectException e) {
 397			this.changeStatus(Account.State.SERVER_NOT_FOUND);
 398		} catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
 399			this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
 400		} catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) {
 401			Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage());
 402			this.changeStatus(Account.State.OFFLINE);
 403			this.attempt = Math.max(0, this.attempt - 1);
 404		} finally {
 405			if (!Thread.currentThread().isInterrupted()) {
 406				forceCloseSocket();
 407			} else {
 408				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": not force closing socket because thread was interrupted");
 409			}
 410		}
 411	}
 412
 413	/**
 414	 * Starts xmpp protocol, call after connecting to socket
 415	 *
 416	 * @return true if server returns with valid xmpp, false otherwise
 417	 */
 418	private boolean startXmpp(Socket socket) throws Exception {
 419		if (Thread.currentThread().isInterrupted()) {
 420			throw new InterruptedException();
 421		}
 422		this.socket = socket;
 423		tagReader = new XmlReader();
 424		if (tagWriter != null) {
 425			tagWriter.forceClose();
 426		}
 427		tagWriter = new TagWriter();
 428		tagWriter.setOutputStream(socket.getOutputStream());
 429		tagReader.setInputStream(socket.getInputStream());
 430		tagWriter.beginDocument();
 431		sendStartStream();
 432		final Tag tag = tagReader.readTag();
 433		return tag != null && tag.isStart("stream");
 434	}
 435
 436	private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException {
 437		final SSLContext sc = SSLSocketHelper.getSSLContext();
 438		MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager();
 439		KeyManager[] keyManager;
 440		if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) {
 441			keyManager = new KeyManager[]{new MyKeyManager()};
 442		} else {
 443			keyManager = null;
 444		}
 445		String domain = account.getJid().getDomain();
 446		sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG());
 447		final SSLSocketFactory factory = sc.getSocketFactory();
 448		final DomainHostnameVerifier verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier(), mInteractive);
 449		return new TlsFactoryVerifier(factory, verifier);
 450	}
 451
 452	@Override
 453	public void run() {
 454		synchronized (this) {
 455			this.mThread = Thread.currentThread();
 456			if (this.mThread.isInterrupted()) {
 457				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": aborting connect because thread was interrupted");
 458				return;
 459			}
 460			forceCloseSocket();
 461		}
 462		connect();
 463	}
 464
 465	private void processStream() throws XmlPullParserException, IOException, NoSuchAlgorithmException {
 466		final CountDownLatch streamCountDownLatch = new CountDownLatch(1);
 467		this.mStreamCountDownLatch = streamCountDownLatch;
 468		Tag nextTag = tagReader.readTag();
 469		while (nextTag != null && !nextTag.isEnd("stream")) {
 470			if (nextTag.isStart("error")) {
 471				processStreamError(nextTag);
 472			} else if (nextTag.isStart("features")) {
 473				processStreamFeatures(nextTag);
 474			} else if (nextTag.isStart("proceed")) {
 475				switchOverToTls(nextTag);
 476			} else if (nextTag.isStart("success")) {
 477				final String challenge = tagReader.readElement(nextTag).getContent();
 478				try {
 479					saslMechanism.getResponse(challenge);
 480				} catch (final SaslMechanism.AuthenticationException e) {
 481					Log.e(Config.LOGTAG, String.valueOf(e));
 482					throw new StateChangingException(Account.State.UNAUTHORIZED);
 483				}
 484				Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in");
 485				account.setKey(Account.PINNED_MECHANISM_KEY,
 486						String.valueOf(saslMechanism.getPriority()));
 487				tagReader.reset();
 488				sendStartStream();
 489				final Tag tag = tagReader.readTag();
 490				if (tag != null && tag.isStart("stream")) {
 491					processStream();
 492				} else {
 493					throw new IOException("server didn't restart stream after successful auth");
 494				}
 495				break;
 496			} else if (nextTag.isStart("failure")) {
 497				final Element failure = tagReader.readElement(nextTag);
 498				if (Namespace.SASL.equals(failure.getNamespace())) {
 499					final String text = failure.findChildContent("text");
 500					if (failure.hasChild("account-disabled") && text != null) {
 501						Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
 502						if (matcher.find()) {
 503							try {
 504								URL url = new URL(text.substring(matcher.start(), matcher.end()));
 505								if (url.getProtocol().equals("https")) {
 506									this.redirectionUrl = url;
 507									throw new StateChangingException(Account.State.PAYMENT_REQUIRED);
 508								}
 509							} catch (MalformedURLException e) {
 510								throw new StateChangingException(Account.State.UNAUTHORIZED);
 511							}
 512						}
 513					}
 514					throw new StateChangingException(Account.State.UNAUTHORIZED);
 515				} else if (Namespace.TLS.equals(failure.getNamespace())) {
 516					throw new StateChangingException(Account.State.TLS_ERROR);
 517				} else {
 518					throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 519				}
 520			} else if (nextTag.isStart("challenge")) {
 521				final String challenge = tagReader.readElement(nextTag).getContent();
 522				final Element response = new Element("response", Namespace.SASL);
 523				try {
 524					response.setContent(saslMechanism.getResponse(challenge));
 525				} catch (final SaslMechanism.AuthenticationException e) {
 526					// TODO: Send auth abort tag.
 527					Log.e(Config.LOGTAG, e.toString());
 528				}
 529				tagWriter.writeElement(response);
 530			} else if (nextTag.isStart("enabled")) {
 531				final Element enabled = tagReader.readElement(nextTag);
 532				if ("true".equals(enabled.getAttribute("resume"))) {
 533					this.streamId = enabled.getAttribute("id");
 534					Log.d(Config.LOGTAG, account.getJid().asBareJid().toString()
 535							+ ": stream management(" + smVersion
 536							+ ") enabled (resumable)");
 537				} else {
 538					Log.d(Config.LOGTAG, account.getJid().asBareJid().toString()
 539							+ ": stream management(" + smVersion + ") enabled");
 540				}
 541				this.stanzasReceived = 0;
 542				this.inSmacksSession = true;
 543				final RequestPacket r = new RequestPacket(smVersion);
 544				tagWriter.writeStanzaAsync(r);
 545			} else if (nextTag.isStart("resumed")) {
 546				this.inSmacksSession = true;
 547				this.isBound = true;
 548				this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
 549				lastPacketReceived = SystemClock.elapsedRealtime();
 550				final Element resumed = tagReader.readElement(nextTag);
 551				final String h = resumed.getAttribute("h");
 552				try {
 553					ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
 554					synchronized (this.mStanzaQueue) {
 555						final int serverCount = Integer.parseInt(h);
 556						if (serverCount < stanzasSent) {
 557							Log.d(Config.LOGTAG, account.getJid().asBareJid().toString()
 558									+ ": session resumed with lost packages");
 559							stanzasSent = serverCount;
 560						} else {
 561							Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": session resumed");
 562						}
 563						acknowledgeStanzaUpTo(serverCount);
 564						for (int i = 0; i < this.mStanzaQueue.size(); ++i) {
 565							failedStanzas.add(mStanzaQueue.valueAt(i));
 566						}
 567						mStanzaQueue.clear();
 568					}
 569					Log.d(Config.LOGTAG, "resending " + failedStanzas.size() + " stanzas");
 570					for (AbstractAcknowledgeableStanza packet : failedStanzas) {
 571						if (packet instanceof MessagePacket) {
 572							MessagePacket message = (MessagePacket) packet;
 573							mXmppConnectionService.markMessage(account,
 574									message.getTo().asBareJid(),
 575									message.getId(),
 576									Message.STATUS_UNSEND);
 577						}
 578						sendPacket(packet);
 579					}
 580				} catch (final NumberFormatException ignored) {
 581				}
 582				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": online with resource " + account.getResource());
 583				changeStatus(Account.State.ONLINE);
 584			} else if (nextTag.isStart("r")) {
 585				tagReader.readElement(nextTag);
 586				if (Config.EXTENDED_SM_LOGGING) {
 587					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": acknowledging stanza #" + this.stanzasReceived);
 588				}
 589				final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
 590				tagWriter.writeStanzaAsync(ack);
 591			} else if (nextTag.isStart("a")) {
 592				boolean accountUiNeedsRefresh = false;
 593				synchronized (NotificationService.CATCHUP_LOCK) {
 594					if (mWaitingForSmCatchup.compareAndSet(true, false)) {
 595						int count = mSmCatchupMessageCounter.get();
 596						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": SM catchup complete (" + count + ")");
 597						accountUiNeedsRefresh = true;
 598						if (count > 0) {
 599							mXmppConnectionService.getNotificationService().finishBacklog(true, account);
 600						}
 601					}
 602				}
 603				if (accountUiNeedsRefresh) {
 604					mXmppConnectionService.updateAccountUi();
 605				}
 606				final Element ack = tagReader.readElement(nextTag);
 607				lastPacketReceived = SystemClock.elapsedRealtime();
 608				try {
 609					synchronized (this.mStanzaQueue) {
 610						final int serverSequence = Integer.parseInt(ack.getAttribute("h"));
 611						acknowledgeStanzaUpTo(serverSequence);
 612					}
 613				} catch (NumberFormatException | NullPointerException e) {
 614					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server send ack without sequence number");
 615				}
 616			} else if (nextTag.isStart("failed")) {
 617				Element failed = tagReader.readElement(nextTag);
 618				try {
 619					final int serverCount = Integer.parseInt(failed.getAttribute("h"));
 620					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed but server acknowledged stanza #" + serverCount);
 621					synchronized (this.mStanzaQueue) {
 622						acknowledgeStanzaUpTo(serverCount);
 623					}
 624				} catch (NumberFormatException | NullPointerException e) {
 625					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed");
 626				}
 627				resetStreamId();
 628				sendBindRequest();
 629			} else if (nextTag.isStart("iq")) {
 630				processIq(nextTag);
 631			} else if (nextTag.isStart("message")) {
 632				processMessage(nextTag);
 633			} else if (nextTag.isStart("presence")) {
 634				processPresence(nextTag);
 635			}
 636			nextTag = tagReader.readTag();
 637		}
 638		if (nextTag != null && nextTag.isEnd("stream")) {
 639			streamCountDownLatch.countDown();
 640		}
 641	}
 642
 643	private void acknowledgeStanzaUpTo(int serverCount) {
 644		if (serverCount > stanzasSent) {
 645			Log.e(Config.LOGTAG,"server acknowledged more stanzas than we sent. serverCount="+serverCount+", ourCount="+stanzasSent);
 646		}
 647		for (int i = 0; i < mStanzaQueue.size(); ++i) {
 648			if (serverCount >= mStanzaQueue.keyAt(i)) {
 649				if (Config.EXTENDED_SM_LOGGING) {
 650					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i));
 651				}
 652				AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
 653				if (stanza instanceof MessagePacket && acknowledgedListener != null) {
 654					MessagePacket packet = (MessagePacket) stanza;
 655					acknowledgedListener.onMessageAcknowledged(account, packet.getId());
 656				}
 657				mStanzaQueue.removeAt(i);
 658				i--;
 659			}
 660		}
 661	}
 662
 663	private Element processPacket(final Tag currentTag, final int packetType)
 664			throws XmlPullParserException, IOException {
 665		Element element;
 666		switch (packetType) {
 667			case PACKET_IQ:
 668				element = new IqPacket();
 669				break;
 670			case PACKET_MESSAGE:
 671				element = new MessagePacket();
 672				break;
 673			case PACKET_PRESENCE:
 674				element = new PresencePacket();
 675				break;
 676			default:
 677				return null;
 678		}
 679		element.setAttributes(currentTag.getAttributes());
 680		Tag nextTag = tagReader.readTag();
 681		if (nextTag == null) {
 682			throw new IOException("interrupted mid tag");
 683		}
 684		while (!nextTag.isEnd(element.getName())) {
 685			if (!nextTag.isNo()) {
 686				final Element child = tagReader.readElement(nextTag);
 687				final String type = currentTag.getAttribute("type");
 688				if (packetType == PACKET_IQ
 689						&& "jingle".equals(child.getName())
 690						&& ("set".equalsIgnoreCase(type) || "get"
 691						.equalsIgnoreCase(type))) {
 692					element = new JinglePacket();
 693					element.setAttributes(currentTag.getAttributes());
 694				}
 695				element.addChild(child);
 696			}
 697			nextTag = tagReader.readTag();
 698			if (nextTag == null) {
 699				throw new IOException("interrupted mid tag");
 700			}
 701		}
 702		if (stanzasReceived == Integer.MAX_VALUE) {
 703			resetStreamId();
 704			throw new IOException("time to restart the session. cant handle >2 billion pcks");
 705		}
 706		if (inSmacksSession) {
 707			++stanzasReceived;
 708		} else if (features.sm()) {
 709			Log.d(Config.LOGTAG,account.getJid().asBareJid()+": not counting stanza("+element.getClass().getSimpleName()+"). Not in smacks session.");
 710		}
 711		lastPacketReceived = SystemClock.elapsedRealtime();
 712		if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
 713			Log.d(Config.LOGTAG, "[background stanza] " + element);
 714		}
 715		return element;
 716	}
 717
 718	private void processIq(final Tag currentTag) throws XmlPullParserException, IOException {
 719		final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
 720
 721		if (packet.getId() == null) {
 722			return; // an iq packet without id is definitely invalid
 723		}
 724
 725		if (packet instanceof JinglePacket) {
 726			if (this.jingleListener != null) {
 727				this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet);
 728			}
 729		} else {
 730			OnIqPacketReceived callback = null;
 731			synchronized (this.packetCallbacks) {
 732				if (packetCallbacks.containsKey(packet.getId())) {
 733					final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId());
 734					// Packets to the server should have responses from the server
 735					if (packetCallbackDuple.first.toServer(account)) {
 736						if (packet.fromServer(account)) {
 737							callback = packetCallbackDuple.second;
 738							packetCallbacks.remove(packet.getId());
 739						} else {
 740							Log.e(Config.LOGTAG, account.getJid().asBareJid().toString() + ": ignoring spoofed iq packet");
 741						}
 742					} else {
 743						if (packet.getFrom() != null && packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
 744							callback = packetCallbackDuple.second;
 745							packetCallbacks.remove(packet.getId());
 746						} else {
 747							Log.e(Config.LOGTAG, account.getJid().asBareJid().toString() + ": ignoring spoofed iq packet");
 748						}
 749					}
 750				} else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
 751					callback = this.unregisteredIqListener;
 752				}
 753			}
 754			if (callback != null) {
 755				try {
 756					callback.onIqPacketReceived(account, packet);
 757				} catch (StateChangingError error) {
 758					throw new StateChangingException(error.state);
 759				}
 760			}
 761		}
 762	}
 763
 764	private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException {
 765		final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
 766		this.messageListener.onMessagePacketReceived(account, packet);
 767	}
 768
 769	private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException {
 770		PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
 771		this.presenceListener.onPresencePacketReceived(account, packet);
 772	}
 773
 774	private void sendStartTLS() throws IOException {
 775		final Tag startTLS = Tag.empty("starttls");
 776		startTLS.setAttribute("xmlns", Namespace.TLS);
 777		tagWriter.writeTag(startTLS);
 778	}
 779
 780	private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
 781		tagReader.readTag();
 782		try {
 783			final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
 784			final InetAddress address = socket == null ? null : socket.getInetAddress();
 785
 786			if (address == null) {
 787				throw new IOException("could not setup ssl");
 788			}
 789
 790			final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
 791
 792			if (sslSocket == null) {
 793				throw new IOException("could not initialize ssl socket");
 794			}
 795
 796			SSLSocketHelper.setSecurity(sslSocket);
 797
 798			if (!tlsFactoryVerifier.verifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
 799				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
 800				throw new StateChangingException(Account.State.TLS_ERROR);
 801			}
 802			tagReader.setInputStream(sslSocket.getInputStream());
 803			tagWriter.setOutputStream(sslSocket.getOutputStream());
 804			sendStartStream();
 805			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
 806			features.encryptionEnabled = true;
 807			final Tag tag = tagReader.readTag();
 808			if (tag != null && tag.isStart("stream")) {
 809				processStream();
 810			} else {
 811				throw new IOException("server didn't restart stream after STARTTLS");
 812			}
 813			sslSocket.close();
 814		} catch (final NoSuchAlgorithmException | KeyManagementException e1) {
 815			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
 816			throw new StateChangingException(Account.State.TLS_ERROR);
 817		}
 818	}
 819
 820	private void processStreamFeatures(final Tag currentTag) throws XmlPullParserException, IOException {
 821		this.streamFeatures = tagReader.readElement(currentTag);
 822		final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS;
 823		final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
 824		if (this.streamFeatures.hasChild("starttls") && !features.encryptionEnabled) {
 825			sendStartTLS();
 826		} else if (this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) {
 827			if (isSecure) {
 828				sendRegistryRequest();
 829			} else {
 830				throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 831			}
 832		} else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) {
 833			throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
 834		} else if (this.streamFeatures.hasChild("mechanisms") && shouldAuthenticate && isSecure) {
 835			authenticate();
 836		} else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) {
 837			if (Config.EXTENDED_SM_LOGGING) {
 838				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resuming after stanza #" + stanzasReceived);
 839			}
 840			final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion);
 841			this.mSmCatchupMessageCounter.set(0);
 842			this.mWaitingForSmCatchup.set(true);
 843			this.tagWriter.writeStanzaAsync(resume);
 844		} else if (needsBinding) {
 845			if (this.streamFeatures.hasChild("bind") && isSecure) {
 846				sendBindRequest();
 847			} else {
 848				throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 849			}
 850		}
 851	}
 852
 853	private void authenticate() throws IOException {
 854		final List<String> mechanisms = extractMechanisms(streamFeatures
 855				.findChild("mechanisms"));
 856		final Element auth = new Element("auth", Namespace.SASL);
 857		if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) {
 858			saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
 859		} else if (mechanisms.contains("SCRAM-SHA-256")) {
 860			saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG());
 861		} else if (mechanisms.contains("SCRAM-SHA-1")) {
 862			saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
 863		} else if (mechanisms.contains("PLAIN") && !account.getJid().getDomain().equals("nimbuzz.com")) {
 864			saslMechanism = new Plain(tagWriter, account);
 865		} else if (mechanisms.contains("DIGEST-MD5")) {
 866			saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
 867		} else if (mechanisms.contains("ANONYMOUS")) {
 868			saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG());
 869		}
 870		if (saslMechanism != null) {
 871			final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1);
 872			if (pinnedMechanism > saslMechanism.getPriority()) {
 873				Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() +
 874						" has lower priority (" + String.valueOf(saslMechanism.getPriority()) +
 875						") than pinned priority (" + pinnedMechanism +
 876						"). Possible downgrade attack?");
 877				throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
 878			}
 879			Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with " + saslMechanism.getMechanism());
 880			auth.setAttribute("mechanism", saslMechanism.getMechanism());
 881			if (!saslMechanism.getClientFirstMessage().isEmpty()) {
 882				auth.setContent(saslMechanism.getClientFirstMessage());
 883			}
 884			tagWriter.writeElement(auth);
 885		} else {
 886			throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 887		}
 888	}
 889
 890	private List<String> extractMechanisms(final Element stream) {
 891		final ArrayList<String> mechanisms = new ArrayList<>(stream
 892				.getChildren().size());
 893		for (final Element child : stream.getChildren()) {
 894			mechanisms.add(child.getContent());
 895		}
 896		return mechanisms;
 897	}
 898
 899	private void sendRegistryRequest() {
 900		final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
 901		register.query("jabber:iq:register");
 902		register.setTo(Jid.of(account.getServer()));
 903		sendUnmodifiedIqPacket(register, (account, packet) -> {
 904			if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
 905				return;
 906			}
 907			if (packet.getType() == IqPacket.TYPE.ERROR) {
 908				throw new StateChangingError(Account.State.REGISTRATION_FAILED);
 909			}
 910			final Element query = packet.query("jabber:iq:register");
 911			if (query.hasChild("username") && (query.hasChild("password"))) {
 912				final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET);
 913				final Element username = new Element("username").setContent(account.getUsername());
 914				final Element password = new Element("password").setContent(account.getPassword());
 915				register1.query("jabber:iq:register").addChild(username);
 916				register1.query().addChild(password);
 917				register1.setFrom(account.getJid().asBareJid());
 918				sendUnmodifiedIqPacket(register1, registrationResponseListener, true);
 919			} else if (query.hasChild("x", Namespace.DATA)) {
 920				final Data data = Data.parse(query.findChild("x", Namespace.DATA));
 921				final Element blob = query.findChild("data", "urn:xmpp:bob");
 922				final String id = packet.getId();
 923				InputStream is;
 924				if (blob != null) {
 925					try {
 926						final String base64Blob = blob.getContent();
 927						final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
 928						is = new ByteArrayInputStream(strBlob);
 929					} catch (Exception e) {
 930						is = null;
 931					}
 932				} else {
 933					try {
 934						Field field = data.getFieldByName("url");
 935						URL url = field != null && field.getValue() != null ? new URL(field.getValue()) : null;
 936						is = url != null ? url.openStream() : null;
 937					} catch (IOException e) {
 938						is = null;
 939					}
 940				}
 941
 942				if (is != null) {
 943					Bitmap captcha = BitmapFactory.decodeStream(is);
 944					try {
 945						if (mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha)) {
 946							return;
 947						}
 948					} catch (Exception e) {
 949						throw new StateChangingError(Account.State.REGISTRATION_FAILED);
 950					}
 951				}
 952				throw new StateChangingError(Account.State.REGISTRATION_FAILED);
 953			} else if (query.hasChild("instructions") || query.hasChild("x",Namespace.OOB)) {
 954				final String instructions = query.findChildContent("instructions");
 955				final Element oob = query.findChild("x", Namespace.OOB);
 956				final String url = oob == null ? null : oob.findChildContent("url");
 957				if (url != null) {
 958					setAccountCreationFailed(url);
 959				} else if (instructions != null) {
 960					Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
 961					if (matcher.find()) {
 962						setAccountCreationFailed(instructions.substring(matcher.start(), matcher.end()));
 963					}
 964				}
 965				throw new StateChangingError(Account.State.REGISTRATION_FAILED);
 966			}
 967		},true);
 968	}
 969
 970	private void setAccountCreationFailed(String url) {
 971		if (url != null) {
 972			try {
 973				this.redirectionUrl = new URL(url);
 974				if (this.redirectionUrl.getProtocol().equals("https")) {
 975					throw new StateChangingError(Account.State.REGISTRATION_WEB);
 976				}
 977			} catch (MalformedURLException e) {
 978				//fall through
 979			}
 980		}
 981		throw new StateChangingError(Account.State.REGISTRATION_FAILED);
 982	}
 983
 984	public URL getRedirectionUrl() {
 985		return this.redirectionUrl;
 986	}
 987
 988	public void resetEverything() {
 989		resetAttemptCount(true);
 990		resetStreamId();
 991		clearIqCallbacks();
 992		this.stanzasSent = 0;
 993		mStanzaQueue.clear();
 994		this.redirectionUrl = null;
 995		synchronized (this.disco) {
 996			disco.clear();
 997		}
 998	}
 999
1000	private void sendBindRequest() {
1001		try {
1002			mXmppConnectionService.restoredFromDatabaseLatch.await();
1003		} catch (InterruptedException e) {
1004			Log.d(Config.LOGTAG,account.getJid().asBareJid()+": interrupted while waiting for DB restore during bind");
1005			return;
1006		}
1007		clearIqCallbacks();
1008		if (account.getJid().isBareJid()) {
1009			account.setResource(this.createNewResource());
1010		} else {
1011			fixResource(mXmppConnectionService, account);
1012		}
1013		final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1014		final String resource = Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource();
1015		iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource);
1016		this.sendUnmodifiedIqPacket(iq, (account, packet) -> {
1017			if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1018				return;
1019			}
1020			final Element bind = packet.findChild("bind");
1021			if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
1022				isBound = true;
1023				final Element jid = bind.findChild("jid");
1024				if (jid != null && jid.getContent() != null) {
1025					try {
1026						Jid assignedJid = Jid.ofEscaped(jid.getContent());
1027						if (!account.getJid().getDomain().equals(assignedJid.getDomain())) {
1028							Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server tried to re-assign domain to "+assignedJid.getDomain());
1029							throw new StateChangingError(Account.State.BIND_FAILURE);
1030						}
1031						if (account.setJid(assignedJid)) {
1032							Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during bind. updating database");
1033							mXmppConnectionService.databaseBackend.updateAccount(account);
1034						}
1035						if (streamFeatures.hasChild("session")
1036								&& !streamFeatures.findChild("session").hasChild("optional")) {
1037							sendStartSession();
1038						} else {
1039							sendPostBindInitialization();
1040						}
1041						return;
1042					} catch (final IllegalArgumentException e) {
1043						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server reported invalid jid (" + jid.getContent() + ") on bind");
1044					}
1045				} else {
1046					Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)");
1047				}
1048			} else {
1049				Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure (" + packet.toString());
1050			}
1051			final Element error = packet.findChild("error");
1052			if (packet.getType() == IqPacket.TYPE.ERROR && error != null && error.hasChild("conflict")) {
1053				account.setResource(createNewResource());
1054			}
1055			throw new StateChangingError(Account.State.BIND_FAILURE);
1056		},true);
1057	}
1058
1059	private void clearIqCallbacks() {
1060		final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
1061		final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
1062		synchronized (this.packetCallbacks) {
1063			if (this.packetCallbacks.size() == 0) {
1064				return;
1065			}
1066			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": clearing " + this.packetCallbacks.size() + " iq callbacks");
1067			final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator = this.packetCallbacks.values().iterator();
1068			while (iterator.hasNext()) {
1069				Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
1070				callbacks.add(entry.second);
1071				iterator.remove();
1072			}
1073		}
1074		for (OnIqPacketReceived callback : callbacks) {
1075			try {
1076				callback.onIqPacketReceived(account, failurePacket);
1077			} catch (StateChangingError error) {
1078				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": caught StateChangingError(" + error.state.toString() + ") while clearing callbacks");
1079				//ignore
1080			}
1081		}
1082		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done clearing iq callbacks. " + this.packetCallbacks.size() + " left");
1083	}
1084
1085	public void sendDiscoTimeout() {
1086		if (mWaitForDisco.compareAndSet(true, false)) {
1087			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": finalizing bind after disco timeout");
1088			finalizeBind();
1089		}
1090	}
1091
1092	private void sendStartSession() {
1093		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending legacy session to outdated server");
1094		final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
1095		startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
1096		this.sendUnmodifiedIqPacket(startSession, (account, packet) -> {
1097			if (packet.getType() == IqPacket.TYPE.RESULT) {
1098				sendPostBindInitialization();
1099			} else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1100				throw new StateChangingError(Account.State.SESSION_FAILURE);
1101			}
1102		},true);
1103	}
1104
1105	private void sendPostBindInitialization() {
1106		smVersion = 0;
1107		if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
1108			smVersion = 3;
1109		} else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
1110			smVersion = 2;
1111		}
1112		if (smVersion != 0) {
1113			synchronized (this.mStanzaQueue) {
1114				final EnablePacket enable = new EnablePacket(smVersion);
1115				tagWriter.writeStanzaAsync(enable);
1116				stanzasSent = 0;
1117				mStanzaQueue.clear();
1118			}
1119		}
1120		features.carbonsEnabled = false;
1121		features.blockListRequested = false;
1122		synchronized (this.disco) {
1123			this.disco.clear();
1124		}
1125		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
1126		mPendingServiceDiscoveries.set(0);
1127		if (smVersion == 0 || Patches.DISCO_EXCEPTIONS.contains(account.getJid().getDomain())) {
1128			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not wait for service discovery");
1129			mWaitForDisco.set(false);
1130		} else {
1131			mWaitForDisco.set(true);
1132		}
1133		lastDiscoStarted = SystemClock.elapsedRealtime();
1134		mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
1135		Element caps = streamFeatures.findChild("c");
1136		final String hash = caps == null ? null : caps.getAttribute("hash");
1137		final String ver = caps == null ? null : caps.getAttribute("ver");
1138		ServiceDiscoveryResult discoveryResult = null;
1139		if (hash != null && ver != null) {
1140			discoveryResult = mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
1141		}
1142		final boolean requestDiscoItemsFirst = !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
1143		if (requestDiscoItemsFirst) {
1144			sendServiceDiscoveryItems(Jid.of(account.getServer()));
1145		}
1146		if (discoveryResult == null) {
1147			sendServiceDiscoveryInfo(Jid.of(account.getServer()));
1148		} else {
1149			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache");
1150			disco.put(Jid.of(account.getServer()), discoveryResult);
1151		}
1152		sendServiceDiscoveryInfo(account.getJid().asBareJid());
1153		if (!requestDiscoItemsFirst) {
1154			sendServiceDiscoveryItems(Jid.of(account.getServer()));
1155		}
1156
1157		if (!mWaitForDisco.get()) {
1158			finalizeBind();
1159		}
1160		this.lastSessionStarted = SystemClock.elapsedRealtime();
1161	}
1162
1163	private void sendServiceDiscoveryInfo(final Jid jid) {
1164		mPendingServiceDiscoveries.incrementAndGet();
1165		final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1166		iq.setTo(jid);
1167		iq.query("http://jabber.org/protocol/disco#info");
1168		this.sendIqPacket(iq, (account, packet) -> {
1169			if (packet.getType() == IqPacket.TYPE.RESULT) {
1170				boolean advancedStreamFeaturesLoaded;
1171				synchronized (XmppConnection.this.disco) {
1172					ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
1173					if (jid.equals(Jid.of(account.getServer()))) {
1174						mXmppConnectionService.databaseBackend.insertDiscoveryResult(result);
1175					}
1176					disco.put(jid, result);
1177					advancedStreamFeaturesLoaded = disco.containsKey(Jid.of(account.getServer()))
1178							&& disco.containsKey(account.getJid().asBareJid());
1179				}
1180				if (advancedStreamFeaturesLoaded && (jid.equals(Jid.of(account.getServer())) || jid.equals(account.getJid().asBareJid()))) {
1181					enableAdvancedStreamFeatures();
1182				}
1183			} else {
1184				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not query disco info for " + jid.toString());
1185			}
1186			if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1187				if (mPendingServiceDiscoveries.decrementAndGet() == 0
1188						&& mWaitForDisco.compareAndSet(true, false)) {
1189					finalizeBind();
1190				}
1191			}
1192		});
1193	}
1194
1195	private void finalizeBind() {
1196		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": online with resource " + account.getResource());
1197		if (bindListener != null) {
1198			bindListener.onBind(account);
1199		}
1200		changeStatus(Account.State.ONLINE);
1201	}
1202
1203	private void enableAdvancedStreamFeatures() {
1204		if (getFeatures().carbons() && !features.carbonsEnabled) {
1205			sendEnableCarbons();
1206		}
1207		if (getFeatures().blocking() && !features.blockListRequested) {
1208			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
1209			this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
1210		}
1211		for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
1212			listener.onAdvancedStreamFeaturesAvailable(account);
1213		}
1214	}
1215
1216	private void sendServiceDiscoveryItems(final Jid server) {
1217		mPendingServiceDiscoveries.incrementAndGet();
1218		final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1219		iq.setTo(Jid.ofDomain(server.getDomain()));
1220		iq.query("http://jabber.org/protocol/disco#items");
1221		this.sendIqPacket(iq, (account, packet) -> {
1222			if (packet.getType() == IqPacket.TYPE.RESULT) {
1223				HashSet<Jid> items = new HashSet<Jid>();
1224				final List<Element> elements = packet.query().getChildren();
1225				for (final Element element : elements) {
1226					if (element.getName().equals("item")) {
1227						final Jid jid = element.getAttributeAsJid("jid");
1228						if (jid != null && !jid.equals(Jid.of(account.getServer()))) {
1229							items.add(jid);
1230						}
1231					}
1232				}
1233				for (Jid jid : items) {
1234					sendServiceDiscoveryInfo(jid);
1235				}
1236			} else {
1237				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not query disco items of " + server);
1238			}
1239			if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1240				if (mPendingServiceDiscoveries.decrementAndGet() == 0
1241						&& mWaitForDisco.compareAndSet(true, false)) {
1242					finalizeBind();
1243				}
1244			}
1245		});
1246	}
1247
1248	private void sendEnableCarbons() {
1249		final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1250		iq.addChild("enable", "urn:xmpp:carbons:2");
1251		this.sendIqPacket(iq, new OnIqPacketReceived() {
1252
1253			@Override
1254			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1255				if (!packet.hasChild("error")) {
1256					Log.d(Config.LOGTAG, account.getJid().asBareJid()
1257							+ ": successfully enabled carbons");
1258					features.carbonsEnabled = true;
1259				} else {
1260					Log.d(Config.LOGTAG, account.getJid().asBareJid()
1261							+ ": error enableing carbons " + packet.toString());
1262				}
1263			}
1264		});
1265	}
1266
1267	private void processStreamError(final Tag currentTag) throws XmlPullParserException, IOException {
1268		final Element streamError = tagReader.readElement(currentTag);
1269		if (streamError == null) {
1270			return;
1271		}
1272		if (streamError.hasChild("conflict")) {
1273			account.setResource(createNewResource());
1274			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": switching resource due to conflict (" + account.getResource() + ")");
1275			throw new IOException();
1276		} else if (streamError.hasChild("host-unknown")) {
1277			throw new StateChangingException(Account.State.HOST_UNKNOWN);
1278		} else if (streamError.hasChild("policy-violation")) {
1279			throw new StateChangingException(Account.State.POLICY_VIOLATION);
1280		} else {
1281			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError.toString());
1282			throw new StateChangingException(Account.State.STREAM_ERROR);
1283		}
1284	}
1285
1286	private void sendStartStream() throws IOException {
1287		final Tag stream = Tag.start("stream:stream");
1288		stream.setAttribute("to", account.getServer());
1289		stream.setAttribute("version", "1.0");
1290		stream.setAttribute("xml:lang", "en");
1291		stream.setAttribute("xmlns", "jabber:client");
1292		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
1293		tagWriter.writeTag(stream);
1294	}
1295
1296	private String createNewResource() {
1297		return mXmppConnectionService.getString(R.string.app_name)+'.'+nextRandomId(true);
1298	}
1299
1300	private static void fixResource(Context context, Account account) {
1301		String resource = account.getResource();
1302		int fixedPartLength = context.getString(R.string.app_name).length() + 1; //include the trailing dot
1303		int randomPartLength = 4; // 3 bytes
1304		if (resource != null && resource.length() > fixedPartLength + randomPartLength) {
1305			if (validBase64(resource.substring(fixedPartLength,fixedPartLength + randomPartLength))) {
1306				account.setResource(resource.substring(0,fixedPartLength + randomPartLength));
1307			}
1308		}
1309	}
1310
1311	private static boolean validBase64(String input) {
1312		try {
1313			return Base64.decode(input, Base64.URL_SAFE).length == 3;
1314		} catch (Throwable throwable) {
1315			return false;
1316		}
1317	}
1318
1319	private String nextRandomId() {
1320		return nextRandomId(false);
1321	}
1322
1323	private String nextRandomId(boolean s) {
1324		return CryptoHelper.random(s ? 3 : 9, mXmppConnectionService.getRNG());
1325	}
1326
1327	public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
1328		packet.setFrom(account.getJid());
1329		return this.sendUnmodifiedIqPacket(packet, callback, false);
1330	}
1331
1332	public synchronized String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback, boolean force) {
1333		if (packet.getId() == null) {
1334			packet.setAttribute("id", nextRandomId());
1335		}
1336		if (callback != null) {
1337			synchronized (this.packetCallbacks) {
1338				packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
1339			}
1340		}
1341		this.sendPacket(packet,force);
1342		return packet.getId();
1343	}
1344
1345	public void sendMessagePacket(final MessagePacket packet) {
1346		this.sendPacket(packet);
1347	}
1348
1349	public void sendPresencePacket(final PresencePacket packet) {
1350		this.sendPacket(packet);
1351	}
1352
1353	private synchronized void sendPacket(final AbstractStanza packet) {
1354		sendPacket(packet,false);
1355	}
1356
1357	private synchronized void sendPacket(final AbstractStanza packet, final boolean force) {
1358		if (stanzasSent == Integer.MAX_VALUE) {
1359			resetStreamId();
1360			disconnect(true);
1361			return;
1362		}
1363		synchronized (this.mStanzaQueue) {
1364			if (force || isBound) {
1365				tagWriter.writeStanzaAsync(packet);
1366			} else {
1367				Log.d(Config.LOGTAG,account.getJid().asBareJid()+" do not write stanza to unbound stream "+packet.toString());
1368			}
1369			if (packet instanceof AbstractAcknowledgeableStanza) {
1370				AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
1371
1372				if (this.mStanzaQueue.size() != 0) {
1373					int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
1374					if (currentHighestKey != stanzasSent) {
1375						throw new AssertionError("Stanza count messed up");
1376					}
1377				}
1378
1379				++stanzasSent;
1380				this.mStanzaQueue.append(stanzasSent, stanza);
1381				if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
1382					if (Config.EXTENDED_SM_LOGGING) {
1383						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": requesting ack for message stanza #" + stanzasSent);
1384					}
1385					tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
1386				}
1387			}
1388		}
1389	}
1390
1391	public void sendPing() {
1392		if (!r()) {
1393			final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1394			iq.setFrom(account.getJid());
1395			iq.addChild("ping", "urn:xmpp:ping");
1396			this.sendIqPacket(iq, null);
1397		}
1398		this.lastPingSent = SystemClock.elapsedRealtime();
1399	}
1400
1401	public void setOnMessagePacketReceivedListener(
1402			final OnMessagePacketReceived listener) {
1403		this.messageListener = listener;
1404	}
1405
1406	public void setOnUnregisteredIqPacketReceivedListener(
1407			final OnIqPacketReceived listener) {
1408		this.unregisteredIqListener = listener;
1409	}
1410
1411	public void setOnPresencePacketReceivedListener(
1412			final OnPresencePacketReceived listener) {
1413		this.presenceListener = listener;
1414	}
1415
1416	public void setOnJinglePacketReceivedListener(
1417			final OnJinglePacketReceived listener) {
1418		this.jingleListener = listener;
1419	}
1420
1421	public void setOnStatusChangedListener(final OnStatusChanged listener) {
1422		this.statusListener = listener;
1423	}
1424
1425	public void setOnBindListener(final OnBindListener listener) {
1426		this.bindListener = listener;
1427	}
1428
1429	public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
1430		this.acknowledgedListener = listener;
1431	}
1432
1433	public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) {
1434		if (!this.advancedStreamFeaturesLoadedListeners.contains(listener)) {
1435			this.advancedStreamFeaturesLoadedListeners.add(listener);
1436		}
1437	}
1438
1439	private void forceCloseSocket() {
1440		if (socket != null) {
1441			try {
1442				socket.close();
1443			} catch (IOException e) {
1444				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception " + e.getMessage() + " during force close");
1445			}
1446		} else {
1447			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": socket was null during force close");
1448		}
1449	}
1450
1451	public void interrupt() {
1452		if (this.mThread != null) {
1453			this.mThread.interrupt();
1454		}
1455	}
1456
1457	public void disconnect(final boolean force) {
1458		interrupt();
1459		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + Boolean.toString(force));
1460		if (force) {
1461			forceCloseSocket();
1462		} else {
1463			final TagWriter currentTagWriter = this.tagWriter;
1464			if (currentTagWriter.isActive()) {
1465				currentTagWriter.finish();
1466				final Socket currentSocket = this.socket;
1467				final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
1468				try {
1469					currentTagWriter.await(1,TimeUnit.SECONDS);
1470					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
1471					currentTagWriter.writeTag(Tag.end("stream:stream"));
1472					if (streamCountDownLatch != null) {
1473							if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
1474							Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": remote ended stream");
1475						} else {
1476							Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": remote has not closed socket. force closing");
1477						}
1478					}
1479				} catch (InterruptedException e) {
1480					Log.d(Config.LOGTAG,account.getJid().asBareJid()+": interrupted while gracefully closing stream");
1481				} catch (final IOException e) {
1482					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during disconnect (" + e.getMessage() + ")");
1483				} finally {
1484					FileBackend.close(currentSocket);
1485				}
1486			} else {
1487				forceCloseSocket();
1488			}
1489		}
1490	}
1491
1492	private void resetStreamId() {
1493		this.streamId = null;
1494	}
1495
1496	private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
1497		synchronized (this.disco) {
1498			final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
1499			for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
1500				if (cursor.getValue().getFeatures().contains(feature)) {
1501					items.add(cursor);
1502				}
1503			}
1504			return items;
1505		}
1506	}
1507
1508	public Jid findDiscoItemByFeature(final String feature) {
1509		final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
1510		if (items.size() >= 1) {
1511			return items.get(0).getKey();
1512		}
1513		return null;
1514	}
1515
1516	public boolean r() {
1517		if (getFeatures().sm()) {
1518			this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1519			return true;
1520		} else {
1521			return false;
1522		}
1523	}
1524
1525	public String getMucServer() {
1526		synchronized (this.disco) {
1527			for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
1528				final ServiceDiscoveryResult value = cursor.getValue();
1529				if (value.getFeatures().contains("http://jabber.org/protocol/muc")
1530						&& !value.getFeatures().contains("jabber:iq:gateway")
1531						&& !value.hasIdentity("conference", "irc")) {
1532					return cursor.getKey().toString();
1533				}
1534			}
1535		}
1536		return null;
1537	}
1538
1539	public int getTimeToNextAttempt() {
1540		final int interval = Math.min((int) (25 * Math.pow(1.3, attempt)), 300);
1541		final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1542		return interval - secondsSinceLast;
1543	}
1544
1545	public int getAttempt() {
1546		return this.attempt;
1547	}
1548
1549	public Features getFeatures() {
1550		return this.features;
1551	}
1552
1553	public long getLastSessionEstablished() {
1554		final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1555		return System.currentTimeMillis() - diff;
1556	}
1557
1558	public long getLastConnect() {
1559		return this.lastConnect;
1560	}
1561
1562	public long getLastPingSent() {
1563		return this.lastPingSent;
1564	}
1565
1566	public long getLastDiscoStarted() {
1567		return this.lastDiscoStarted;
1568	}
1569
1570	public long getLastPacketReceived() {
1571		return this.lastPacketReceived;
1572	}
1573
1574	public void sendActive() {
1575		this.sendPacket(new ActivePacket());
1576	}
1577
1578	public void sendInactive() {
1579		this.sendPacket(new InactivePacket());
1580	}
1581
1582	public void resetAttemptCount(boolean resetConnectTime) {
1583		this.attempt = 0;
1584		if (resetConnectTime) {
1585			this.lastConnect = 0;
1586		}
1587	}
1588
1589	public void setInteractive(boolean interactive) {
1590		this.mInteractive = interactive;
1591	}
1592
1593	public Identity getServerIdentity() {
1594		synchronized (this.disco) {
1595			ServiceDiscoveryResult result = disco.get(Jid.ofDomain(account.getJid().getDomain()));
1596			if (result == null) {
1597				return Identity.UNKNOWN;
1598			}
1599			for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
1600				if (id.getType().equals("im") && id.getCategory().equals("server") && id.getName() != null) {
1601					switch (id.getName()) {
1602						case "Prosody":
1603							return Identity.PROSODY;
1604						case "ejabberd":
1605							return Identity.EJABBERD;
1606						case "Slack-XMPP":
1607							return Identity.SLACK;
1608					}
1609				}
1610			}
1611		}
1612		return Identity.UNKNOWN;
1613	}
1614
1615	private IqGenerator getIqGenerator() {
1616		return mXmppConnectionService.getIqGenerator();
1617	}
1618
1619	public enum Identity {
1620		FACEBOOK,
1621		SLACK,
1622		EJABBERD,
1623		PROSODY,
1624		NIMBUZZ,
1625		UNKNOWN
1626	}
1627
1628	private static class TlsFactoryVerifier {
1629		private final SSLSocketFactory factory;
1630		private final DomainHostnameVerifier verifier;
1631
1632		TlsFactoryVerifier(final SSLSocketFactory factory, final DomainHostnameVerifier verifier) throws IOException {
1633			this.factory = factory;
1634			this.verifier = verifier;
1635			if (factory == null || verifier == null) {
1636				throw new IOException("could not setup ssl");
1637			}
1638		}
1639	}
1640
1641	private class MyKeyManager implements X509KeyManager {
1642		@Override
1643		public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
1644			return account.getPrivateKeyAlias();
1645		}
1646
1647		@Override
1648		public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
1649			return null;
1650		}
1651
1652		@Override
1653		public X509Certificate[] getCertificateChain(String alias) {
1654			Log.d(Config.LOGTAG, "getting certificate chain");
1655			try {
1656				return KeyChain.getCertificateChain(mXmppConnectionService, alias);
1657			} catch (Exception e) {
1658				Log.d(Config.LOGTAG, e.getMessage());
1659				return new X509Certificate[0];
1660			}
1661		}
1662
1663		@Override
1664		public String[] getClientAliases(String s, Principal[] principals) {
1665			final String alias = account.getPrivateKeyAlias();
1666			return alias != null ? new String[]{alias} : new String[0];
1667		}
1668
1669		@Override
1670		public String[] getServerAliases(String s, Principal[] principals) {
1671			return new String[0];
1672		}
1673
1674		@Override
1675		public PrivateKey getPrivateKey(String alias) {
1676			try {
1677				return KeyChain.getPrivateKey(mXmppConnectionService, alias);
1678			} catch (Exception e) {
1679				return null;
1680			}
1681		}
1682	}
1683
1684	private class StateChangingError extends Error {
1685		private final Account.State state;
1686
1687		public StateChangingError(Account.State state) {
1688			this.state = state;
1689		}
1690	}
1691
1692	private class StateChangingException extends IOException {
1693		private final Account.State state;
1694
1695		public StateChangingException(Account.State state) {
1696			this.state = state;
1697		}
1698	}
1699
1700	public class Features {
1701		XmppConnection connection;
1702		private boolean carbonsEnabled = false;
1703		private boolean encryptionEnabled = false;
1704		private boolean blockListRequested = false;
1705
1706		public Features(final XmppConnection connection) {
1707			this.connection = connection;
1708		}
1709
1710		private boolean hasDiscoFeature(final Jid server, final String feature) {
1711			synchronized (XmppConnection.this.disco) {
1712				return connection.disco.containsKey(server) &&
1713						connection.disco.get(server).getFeatures().contains(feature);
1714			}
1715		}
1716
1717		public boolean carbons() {
1718			return hasDiscoFeature(Jid.of(account.getServer()), "urn:xmpp:carbons:2");
1719		}
1720
1721		public boolean blocking() {
1722			return hasDiscoFeature(Jid.of(account.getServer()), Namespace.BLOCKING);
1723		}
1724
1725		public boolean spamReporting() {
1726			return hasDiscoFeature(Jid.of(account.getServer()), "urn:xmpp:reporting:reason:spam:0");
1727		}
1728
1729		public boolean flexibleOfflineMessageRetrieval() {
1730			return hasDiscoFeature(Jid.of(account.getServer()), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
1731		}
1732
1733		public boolean register() {
1734			return hasDiscoFeature(Jid.of(account.getServer()), Namespace.REGISTER);
1735		}
1736
1737		public boolean sm() {
1738			return streamId != null
1739					|| (connection.streamFeatures != null && connection.streamFeatures.hasChild("sm"));
1740		}
1741
1742		public boolean csi() {
1743			return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
1744		}
1745
1746		public boolean pep() {
1747			synchronized (XmppConnection.this.disco) {
1748				ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
1749				return info != null && info.hasIdentity("pubsub", "pep");
1750			}
1751		}
1752
1753		public boolean pepPersistent() {
1754			synchronized (XmppConnection.this.disco) {
1755				ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
1756				return info != null && info.getFeatures().contains("http://jabber.org/protocol/pubsub#persistent-items");
1757			}
1758		}
1759
1760		public boolean pepPublishOptions() {
1761			return hasDiscoFeature(account.getJid().asBareJid(),Namespace.PUBSUB_PUBLISH_OPTIONS);
1762		}
1763
1764		public boolean pepOmemoWhitelisted() {
1765			return hasDiscoFeature(account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
1766		}
1767
1768		public boolean mam() {
1769			return hasDiscoFeature(account.getJid().asBareJid(), Namespace.MAM)
1770					|| hasDiscoFeature(account.getJid().asBareJid(), Namespace.MAM_LEGACY);
1771		}
1772
1773		public boolean mamLegacy() {
1774			return !hasDiscoFeature(account.getJid().asBareJid(), Namespace.MAM)
1775					&& hasDiscoFeature(account.getJid().asBareJid(), Namespace.MAM_LEGACY);
1776		}
1777
1778		public boolean push() {
1779			return hasDiscoFeature(account.getJid().asBareJid(), "urn:xmpp:push:0")
1780					|| hasDiscoFeature(Jid.of(account.getServer()), "urn:xmpp:push:0");
1781		}
1782
1783		public boolean rosterVersioning() {
1784			return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
1785		}
1786
1787		public void setBlockListRequested(boolean value) {
1788			this.blockListRequested = value;
1789		}
1790
1791		public boolean httpUpload(long filesize) {
1792			if (Config.DISABLE_HTTP_UPLOAD) {
1793				return false;
1794			} else {
1795				List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Namespace.HTTP_UPLOAD);
1796				if (items.size() > 0) {
1797					try {
1798						long maxsize = Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Namespace.HTTP_UPLOAD, "max-file-size"));
1799						if (filesize <= maxsize) {
1800							return true;
1801						} else {
1802							Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": http upload is not available for files with size " + filesize + " (max is " + maxsize + ")");
1803							return false;
1804						}
1805					} catch (Exception e) {
1806						return true;
1807					}
1808				} else {
1809					return false;
1810				}
1811			}
1812		}
1813
1814		public long getMaxHttpUploadSize() {
1815			List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Namespace.HTTP_UPLOAD);
1816			if (items.size() > 0) {
1817				try {
1818					return Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Namespace.HTTP_UPLOAD, "max-file-size"));
1819				} catch (Exception e) {
1820					return -1;
1821				}
1822			} else {
1823				return -1;
1824			}
1825		}
1826
1827		public boolean stanzaIds() {
1828			return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
1829		}
1830	}
1831}