XmppConnection.java

   1package eu.siacs.conversations.xmpp;
   2
   3import android.graphics.Bitmap;
   4import android.graphics.BitmapFactory;
   5import android.os.SystemClock;
   6import android.security.KeyChain;
   7import android.util.Base64;
   8import android.util.Log;
   9import android.util.Pair;
  10import android.util.SparseArray;
  11
  12import org.xmlpull.v1.XmlPullParserException;
  13
  14import java.io.ByteArrayInputStream;
  15import java.io.IOException;
  16import java.io.InputStream;
  17import java.net.ConnectException;
  18import java.net.IDN;
  19import java.net.InetAddress;
  20import java.net.InetSocketAddress;
  21import java.net.MalformedURLException;
  22import java.net.Socket;
  23import java.net.URL;
  24import java.net.UnknownHostException;
  25import java.security.KeyManagementException;
  26import java.security.NoSuchAlgorithmException;
  27import java.security.Principal;
  28import java.security.PrivateKey;
  29import java.security.cert.X509Certificate;
  30import java.util.ArrayList;
  31import java.util.Arrays;
  32import java.util.HashMap;
  33import java.util.HashSet;
  34import java.util.Hashtable;
  35import java.util.Iterator;
  36import java.util.List;
  37import java.util.Map.Entry;
  38import java.util.concurrent.CountDownLatch;
  39import java.util.concurrent.TimeUnit;
  40import java.util.concurrent.atomic.AtomicBoolean;
  41import java.util.concurrent.atomic.AtomicInteger;
  42import java.util.regex.Matcher;
  43
  44import javax.net.ssl.KeyManager;
  45import javax.net.ssl.SSLContext;
  46import javax.net.ssl.SSLSession;
  47import javax.net.ssl.SSLSocket;
  48import javax.net.ssl.SSLSocketFactory;
  49import javax.net.ssl.X509KeyManager;
  50import javax.net.ssl.X509TrustManager;
  51
  52import eu.siacs.conversations.Config;
  53import eu.siacs.conversations.R;
  54import eu.siacs.conversations.crypto.DomainHostnameVerifier;
  55import eu.siacs.conversations.crypto.XmppDomainVerifier;
  56import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  57import eu.siacs.conversations.crypto.sasl.Anonymous;
  58import eu.siacs.conversations.crypto.sasl.DigestMd5;
  59import eu.siacs.conversations.crypto.sasl.External;
  60import eu.siacs.conversations.crypto.sasl.Plain;
  61import eu.siacs.conversations.crypto.sasl.SaslMechanism;
  62import eu.siacs.conversations.crypto.sasl.ScramSha1;
  63import eu.siacs.conversations.crypto.sasl.ScramSha256;
  64import eu.siacs.conversations.entities.Account;
  65import eu.siacs.conversations.entities.Message;
  66import eu.siacs.conversations.entities.ServiceDiscoveryResult;
  67import eu.siacs.conversations.generator.IqGenerator;
  68import eu.siacs.conversations.persistance.FileBackend;
  69import eu.siacs.conversations.services.MemorizingTrustManager;
  70import eu.siacs.conversations.services.NotificationService;
  71import eu.siacs.conversations.services.XmppConnectionService;
  72import eu.siacs.conversations.utils.CryptoHelper;
  73import eu.siacs.conversations.utils.IP;
  74import eu.siacs.conversations.utils.Patterns;
  75import eu.siacs.conversations.utils.Resolver;
  76import eu.siacs.conversations.utils.SSLSocketHelper;
  77import eu.siacs.conversations.utils.SocksSocketFactory;
  78import eu.siacs.conversations.xml.Element;
  79import eu.siacs.conversations.xml.Tag;
  80import eu.siacs.conversations.xml.TagWriter;
  81import eu.siacs.conversations.xml.XmlReader;
  82import eu.siacs.conversations.xml.Namespace;
  83import eu.siacs.conversations.xmpp.forms.Data;
  84import eu.siacs.conversations.xmpp.forms.Field;
  85import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  86import eu.siacs.conversations.xmpp.jid.Jid;
  87import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  88import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  89import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza;
  90import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
  91import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  92import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  93import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  94import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
  95import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
  96import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
  97import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
  98import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
  99import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
 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().toBareJid().toPreppedString();
 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().toBareJid() + ": 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().toBareJid().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().toBareJid() + ": 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().toBareJid() + ": 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().getDomainpart();
 271							if (!tlsFactoryVerifier.verifier.verify(domain, this.verifiedHostname, session)) {
 272								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": 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().toBareJid() + ": 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().toString())) {
 294				localSocket = new Socket();
 295				try {
 296					localSocket.connect(new InetSocketAddress(account.getServer().toString(), 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().toBareJid() + ": 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().getDomainpart();
 310				List<Resolver.Result> results = Resolver.resolve(account.getJid().getDomainpart());
 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().toBareJid() + ": 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().toBareJid() + ": 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().toBareJid().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().toBareJid().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().getDomainpart());
 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().getDomainpart(), verifiedHostname, ((SSLSocket) localSocket).getSession())) {
 362								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": 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().toBareJid() + ": thread was interrupted before beginning stream");
 380						return;
 381					} catch (final Throwable e) {
 382						Log.d(Config.LOGTAG, account.getJid().toBareJid().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().toBareJid().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().toBareJid() + ": 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().getDomainpart();
 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().toBareJid() + ": 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().toBareJid().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().toBareJid().toString()
 535							+ ": stream management(" + smVersion
 536							+ ") enabled (resumable)");
 537				} else {
 538					Log.d(Config.LOGTAG, account.getJid().toBareJid().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().toBareJid().toString()
 558									+ ": session resumed with lost packages");
 559							stanzasSent = serverCount;
 560						} else {
 561							Log.d(Config.LOGTAG, account.getJid().toBareJid().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().toBareJid(),
 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().toBareJid() + ": 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().toBareJid() + ": 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().toBareJid() + ": 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().toBareJid() + ": 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().toBareJid() + ": 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().toBareJid() + ": 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().toBareJid() + ": 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().toBareJid()+": 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().toBareJid().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().toBareJid().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().getDomainpart(), this.verifiedHostname, sslSocket.getSession())) {
 799				Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": 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().toBareJid() + ": 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().toBareJid() + ": 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().toBareJid() + ": 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().getDomainpart().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(account.getServer());
 903		sendUnmodifiedIqPacket(register, new OnIqPacketReceived() {
 904
 905			@Override
 906			public void onIqPacketReceived(final Account account, final IqPacket packet) {
 907				if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
 908					return;
 909				}
 910				if (packet.getType() == IqPacket.TYPE.ERROR) {
 911					throw new StateChangingError(Account.State.REGISTRATION_FAILED);
 912				}
 913				final Element query = packet.query("jabber:iq:register");
 914				if (query.hasChild("username") && (query.hasChild("password"))) {
 915					final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
 916					final Element username = new Element("username").setContent(account.getUsername());
 917					final Element password = new Element("password").setContent(account.getPassword());
 918					register.query("jabber:iq:register").addChild(username);
 919					register.query().addChild(password);
 920					register.setFrom(account.getJid().toBareJid());
 921					sendUnmodifiedIqPacket(register, registrationResponseListener, true);
 922				} else if (query.hasChild("x", Namespace.DATA)) {
 923					final Data data = Data.parse(query.findChild("x", Namespace.DATA));
 924					final Element blob = query.findChild("data", "urn:xmpp:bob");
 925					final String id = packet.getId();
 926					InputStream is;
 927					if (blob != null) {
 928						try {
 929							final String base64Blob = blob.getContent();
 930							final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
 931							is = new ByteArrayInputStream(strBlob);
 932						} catch (Exception e) {
 933							is = null;
 934						}
 935					} else {
 936						try {
 937							Field field = data.getFieldByName("url");
 938							URL url = field != null && field.getValue() != null ? new URL(field.getValue()) : null;
 939							is = url != null ? url.openStream() : null;
 940						} catch (IOException e) {
 941							is = null;
 942						}
 943					}
 944
 945					if (is != null) {
 946						Bitmap captcha = BitmapFactory.decodeStream(is);
 947						try {
 948							if (mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha)) {
 949								return;
 950							}
 951						} catch (Exception e) {
 952							throw new StateChangingError(Account.State.REGISTRATION_FAILED);
 953						}
 954					}
 955					throw new StateChangingError(Account.State.REGISTRATION_FAILED);
 956				} else if (query.hasChild("instructions") || query.hasChild("x",Namespace.OOB)) {
 957					final String instructions = query.findChildContent("instructions");
 958					final Element oob = query.findChild("x", Namespace.OOB);
 959					final String url = oob == null ? null : oob.findChildContent("url");
 960					if (url != null) {
 961						setAccountCreationFailed(url);
 962					} else if (instructions != null) {
 963						Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
 964						if (matcher.find()) {
 965							setAccountCreationFailed(instructions.substring(matcher.start(), matcher.end()));
 966						}
 967					}
 968					throw new StateChangingError(Account.State.REGISTRATION_FAILED);
 969				}
 970			}
 971		},true);
 972	}
 973
 974	private void setAccountCreationFailed(String url) {
 975		if (url != null) {
 976			try {
 977				this.redirectionUrl = new URL(url);
 978				if (this.redirectionUrl.getProtocol().equals("https")) {
 979					throw new StateChangingError(Account.State.REGISTRATION_WEB);
 980				}
 981			} catch (MalformedURLException e) {
 982				//fall through
 983			}
 984		}
 985		throw new StateChangingError(Account.State.REGISTRATION_FAILED);
 986	}
 987
 988	public URL getRedirectionUrl() {
 989		return this.redirectionUrl;
 990	}
 991
 992	public void resetEverything() {
 993		resetAttemptCount(true);
 994		resetStreamId();
 995		clearIqCallbacks();
 996		this.stanzasSent = 0;
 997		mStanzaQueue.clear();
 998		this.redirectionUrl = null;
 999		synchronized (this.disco) {
1000			disco.clear();
1001		}
1002	}
1003
1004	private void sendBindRequest() {
1005		try {
1006			mXmppConnectionService.restoredFromDatabaseLatch.await();
1007		} catch (InterruptedException e) {
1008			Log.d(Config.LOGTAG,account.getJid().toBareJid()+": interrupted while waiting for DB restore during bind");
1009			return;
1010		}
1011		clearIqCallbacks();
1012		if (account.getJid().isBareJid()) {
1013			account.setResource(this.createNewResource());
1014		}
1015		final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1016		final String resource = Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource();
1017		iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource);
1018		this.sendUnmodifiedIqPacket(iq, (account, packet) -> {
1019			if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1020				return;
1021			}
1022			final Element bind = packet.findChild("bind");
1023			if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
1024				isBound = true;
1025				final Element jid = bind.findChild("jid");
1026				if (jid != null && jid.getContent() != null) {
1027					try {
1028						Jid assignedJid = Jid.fromString(jid.getContent());
1029						if (!account.getJid().getDomainpart().equals(assignedJid.getDomainpart())) {
1030							Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server tried to re-assign domain to "+assignedJid.getDomainpart());
1031							throw new StateChangingError(Account.State.BIND_FAILURE);
1032						}
1033						if (account.setJid(assignedJid)) {
1034							Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": jid changed during bind. updating database");
1035							mXmppConnectionService.databaseBackend.updateAccount(account);
1036						}
1037						if (streamFeatures.hasChild("session")
1038								&& !streamFeatures.findChild("session").hasChild("optional")) {
1039							sendStartSession();
1040						} else {
1041							sendPostBindInitialization();
1042						}
1043						return;
1044					} catch (final InvalidJidException e) {
1045						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server reported invalid jid (" + jid.getContent() + ") on bind");
1046					}
1047				} else {
1048					Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)");
1049				}
1050			} else {
1051				Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure (" + packet.toString());
1052			}
1053			final Element error = packet.findChild("error");
1054			if (packet.getType() == IqPacket.TYPE.ERROR && error != null && error.hasChild("conflict")) {
1055				account.setResource(createNewResource());
1056			}
1057			throw new StateChangingError(Account.State.BIND_FAILURE);
1058		},true);
1059	}
1060
1061	private void clearIqCallbacks() {
1062		final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
1063		final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
1064		synchronized (this.packetCallbacks) {
1065			if (this.packetCallbacks.size() == 0) {
1066				return;
1067			}
1068			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": clearing " + this.packetCallbacks.size() + " iq callbacks");
1069			final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator = this.packetCallbacks.values().iterator();
1070			while (iterator.hasNext()) {
1071				Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
1072				callbacks.add(entry.second);
1073				iterator.remove();
1074			}
1075		}
1076		for (OnIqPacketReceived callback : callbacks) {
1077			try {
1078				callback.onIqPacketReceived(account, failurePacket);
1079			} catch (StateChangingError error) {
1080				Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": caught StateChangingError(" + error.state.toString() + ") while clearing callbacks");
1081				//ignore
1082			}
1083		}
1084		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": done clearing iq callbacks. " + this.packetCallbacks.size() + " left");
1085	}
1086
1087	public void sendDiscoTimeout() {
1088		if (mWaitForDisco.compareAndSet(true, false)) {
1089			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": finalizing bind after disco timeout");
1090			finalizeBind();
1091		}
1092	}
1093
1094	private void sendStartSession() {
1095		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending legacy session to outdated server");
1096		final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
1097		startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
1098		this.sendUnmodifiedIqPacket(startSession, (account, packet) -> {
1099			if (packet.getType() == IqPacket.TYPE.RESULT) {
1100				sendPostBindInitialization();
1101			} else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1102				throw new StateChangingError(Account.State.SESSION_FAILURE);
1103			}
1104		},true);
1105	}
1106
1107	private void sendPostBindInitialization() {
1108		smVersion = 0;
1109		if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
1110			smVersion = 3;
1111		} else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
1112			smVersion = 2;
1113		}
1114		if (smVersion != 0) {
1115			synchronized (this.mStanzaQueue) {
1116				final EnablePacket enable = new EnablePacket(smVersion);
1117				tagWriter.writeStanzaAsync(enable);
1118				stanzasSent = 0;
1119				mStanzaQueue.clear();
1120			}
1121		}
1122		features.carbonsEnabled = false;
1123		features.blockListRequested = false;
1124		synchronized (this.disco) {
1125			this.disco.clear();
1126		}
1127		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery");
1128		mPendingServiceDiscoveries.set(0);
1129		if (smVersion == 0 || Patches.DISCO_EXCEPTIONS.contains(account.getJid().getDomainpart())) {
1130			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": do not wait for service discovery");
1131			mWaitForDisco.set(false);
1132		} else {
1133			mWaitForDisco.set(true);
1134		}
1135		lastDiscoStarted = SystemClock.elapsedRealtime();
1136		mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
1137		Element caps = streamFeatures.findChild("c");
1138		final String hash = caps == null ? null : caps.getAttribute("hash");
1139		final String ver = caps == null ? null : caps.getAttribute("ver");
1140		ServiceDiscoveryResult discoveryResult = null;
1141		if (hash != null && ver != null) {
1142			discoveryResult = mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
1143		}
1144		final boolean requestDiscoItemsFirst = !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
1145		if (requestDiscoItemsFirst) {
1146			sendServiceDiscoveryItems(account.getServer());
1147		}
1148		if (discoveryResult == null) {
1149			sendServiceDiscoveryInfo(account.getServer());
1150		} else {
1151			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server caps came from cache");
1152			disco.put(account.getServer(), discoveryResult);
1153		}
1154		sendServiceDiscoveryInfo(account.getJid().toBareJid());
1155		if (!requestDiscoItemsFirst) {
1156			sendServiceDiscoveryItems(account.getServer());
1157		}
1158
1159		if (!mWaitForDisco.get()) {
1160			finalizeBind();
1161		}
1162		this.lastSessionStarted = SystemClock.elapsedRealtime();
1163	}
1164
1165	private void sendServiceDiscoveryInfo(final Jid jid) {
1166		mPendingServiceDiscoveries.incrementAndGet();
1167		final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1168		iq.setTo(jid);
1169		iq.query("http://jabber.org/protocol/disco#info");
1170		this.sendIqPacket(iq, new OnIqPacketReceived() {
1171
1172			@Override
1173			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1174				if (packet.getType() == IqPacket.TYPE.RESULT) {
1175					boolean advancedStreamFeaturesLoaded;
1176					synchronized (XmppConnection.this.disco) {
1177						ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
1178						if (jid.equals(account.getServer())) {
1179							mXmppConnectionService.databaseBackend.insertDiscoveryResult(result);
1180						}
1181						disco.put(jid, result);
1182						advancedStreamFeaturesLoaded = disco.containsKey(account.getServer())
1183								&& disco.containsKey(account.getJid().toBareJid());
1184					}
1185					if (advancedStreamFeaturesLoaded && (jid.equals(account.getServer()) || jid.equals(account.getJid().toBareJid()))) {
1186						enableAdvancedStreamFeatures();
1187					}
1188				} else {
1189					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco info for " + jid.toString());
1190				}
1191				if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1192					if (mPendingServiceDiscoveries.decrementAndGet() == 0
1193							&& mWaitForDisco.compareAndSet(true, false)) {
1194						finalizeBind();
1195					}
1196				}
1197			}
1198		});
1199	}
1200
1201	private void finalizeBind() {
1202		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource());
1203		if (bindListener != null) {
1204			bindListener.onBind(account);
1205		}
1206		changeStatus(Account.State.ONLINE);
1207	}
1208
1209	private void enableAdvancedStreamFeatures() {
1210		if (getFeatures().carbons() && !features.carbonsEnabled) {
1211			sendEnableCarbons();
1212		}
1213		if (getFeatures().blocking() && !features.blockListRequested) {
1214			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": Requesting block list");
1215			this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
1216		}
1217		for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
1218			listener.onAdvancedStreamFeaturesAvailable(account);
1219		}
1220	}
1221
1222	private void sendServiceDiscoveryItems(final Jid server) {
1223		mPendingServiceDiscoveries.incrementAndGet();
1224		final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1225		iq.setTo(server.toDomainJid());
1226		iq.query("http://jabber.org/protocol/disco#items");
1227		this.sendIqPacket(iq, new OnIqPacketReceived() {
1228
1229			@Override
1230			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1231				if (packet.getType() == IqPacket.TYPE.RESULT) {
1232					HashSet<Jid> items = new HashSet<Jid>();
1233					final List<Element> elements = packet.query().getChildren();
1234					for (final Element element : elements) {
1235						if (element.getName().equals("item")) {
1236							final Jid jid = element.getAttributeAsJid("jid");
1237							if (jid != null && !jid.equals(account.getServer())) {
1238								items.add(jid);
1239							}
1240						}
1241					}
1242					for (Jid jid : items) {
1243						sendServiceDiscoveryInfo(jid);
1244					}
1245				} else {
1246					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco items of " + server);
1247				}
1248				if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1249					if (mPendingServiceDiscoveries.decrementAndGet() == 0
1250							&& mWaitForDisco.compareAndSet(true, false)) {
1251						finalizeBind();
1252					}
1253				}
1254			}
1255		});
1256	}
1257
1258	private void sendEnableCarbons() {
1259		final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1260		iq.addChild("enable", "urn:xmpp:carbons:2");
1261		this.sendIqPacket(iq, new OnIqPacketReceived() {
1262
1263			@Override
1264			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1265				if (!packet.hasChild("error")) {
1266					Log.d(Config.LOGTAG, account.getJid().toBareJid()
1267							+ ": successfully enabled carbons");
1268					features.carbonsEnabled = true;
1269				} else {
1270					Log.d(Config.LOGTAG, account.getJid().toBareJid()
1271							+ ": error enableing carbons " + packet.toString());
1272				}
1273			}
1274		});
1275	}
1276
1277	private void processStreamError(final Tag currentTag) throws XmlPullParserException, IOException {
1278		final Element streamError = tagReader.readElement(currentTag);
1279		if (streamError == null) {
1280			return;
1281		}
1282		if (streamError.hasChild("conflict")) {
1283			account.setResource(createNewResource());
1284			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": switching resource due to conflict (" + account.getResource() + ")");
1285			throw new IOException();
1286		} else if (streamError.hasChild("host-unknown")) {
1287			throw new StateChangingException(Account.State.HOST_UNKNOWN);
1288		} else if (streamError.hasChild("policy-violation")) {
1289			throw new StateChangingException(Account.State.POLICY_VIOLATION);
1290		} else {
1291			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": stream error " + streamError.toString());
1292			throw new StateChangingException(Account.State.STREAM_ERROR);
1293		}
1294	}
1295
1296	private void sendStartStream() throws IOException {
1297		final Tag stream = Tag.start("stream:stream");
1298		stream.setAttribute("to", account.getServer().toString());
1299		stream.setAttribute("version", "1.0");
1300		stream.setAttribute("xml:lang", "en");
1301		stream.setAttribute("xmlns", "jabber:client");
1302		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
1303		tagWriter.writeTag(stream);
1304	}
1305
1306	private String createNewResource() {
1307		return mXmppConnectionService.getString(R.string.app_name)+'.'+nextRandomId(true);
1308	}
1309
1310	private String nextRandomId() {
1311		return nextRandomId(false);
1312	}
1313
1314	private String nextRandomId(boolean s) {
1315		return CryptoHelper.random(s ? 3 : 9, mXmppConnectionService.getRNG());
1316	}
1317
1318	public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
1319		packet.setFrom(account.getJid());
1320		return this.sendUnmodifiedIqPacket(packet, callback, false);
1321	}
1322
1323	public synchronized String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback, boolean force) {
1324		if (packet.getId() == null) {
1325			packet.setAttribute("id", nextRandomId());
1326		}
1327		if (callback != null) {
1328			synchronized (this.packetCallbacks) {
1329				packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
1330			}
1331		}
1332		this.sendPacket(packet,force);
1333		return packet.getId();
1334	}
1335
1336	public void sendMessagePacket(final MessagePacket packet) {
1337		this.sendPacket(packet);
1338	}
1339
1340	public void sendPresencePacket(final PresencePacket packet) {
1341		this.sendPacket(packet);
1342	}
1343
1344	private synchronized void sendPacket(final AbstractStanza packet) {
1345		sendPacket(packet,false);
1346	}
1347
1348	private synchronized void sendPacket(final AbstractStanza packet, final boolean force) {
1349		if (stanzasSent == Integer.MAX_VALUE) {
1350			resetStreamId();
1351			disconnect(true);
1352			return;
1353		}
1354		synchronized (this.mStanzaQueue) {
1355			if (force || isBound) {
1356				tagWriter.writeStanzaAsync(packet);
1357			} else {
1358				Log.d(Config.LOGTAG,account.getJid().toBareJid()+" do not write stanza to unbound stream "+packet.toString());
1359			}
1360			if (packet instanceof AbstractAcknowledgeableStanza) {
1361				AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
1362
1363				if (this.mStanzaQueue.size() != 0) {
1364					int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
1365					if (currentHighestKey != stanzasSent) {
1366						throw new AssertionError("Stanza count messed up");
1367					}
1368				}
1369
1370				++stanzasSent;
1371				this.mStanzaQueue.append(stanzasSent, stanza);
1372				if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
1373					if (Config.EXTENDED_SM_LOGGING) {
1374						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for message stanza #" + stanzasSent);
1375					}
1376					tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
1377				}
1378			}
1379		}
1380	}
1381
1382	public void sendPing() {
1383		if (!r()) {
1384			final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1385			iq.setFrom(account.getJid());
1386			iq.addChild("ping", "urn:xmpp:ping");
1387			this.sendIqPacket(iq, null);
1388		}
1389		this.lastPingSent = SystemClock.elapsedRealtime();
1390	}
1391
1392	public void setOnMessagePacketReceivedListener(
1393			final OnMessagePacketReceived listener) {
1394		this.messageListener = listener;
1395	}
1396
1397	public void setOnUnregisteredIqPacketReceivedListener(
1398			final OnIqPacketReceived listener) {
1399		this.unregisteredIqListener = listener;
1400	}
1401
1402	public void setOnPresencePacketReceivedListener(
1403			final OnPresencePacketReceived listener) {
1404		this.presenceListener = listener;
1405	}
1406
1407	public void setOnJinglePacketReceivedListener(
1408			final OnJinglePacketReceived listener) {
1409		this.jingleListener = listener;
1410	}
1411
1412	public void setOnStatusChangedListener(final OnStatusChanged listener) {
1413		this.statusListener = listener;
1414	}
1415
1416	public void setOnBindListener(final OnBindListener listener) {
1417		this.bindListener = listener;
1418	}
1419
1420	public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
1421		this.acknowledgedListener = listener;
1422	}
1423
1424	public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) {
1425		if (!this.advancedStreamFeaturesLoadedListeners.contains(listener)) {
1426			this.advancedStreamFeaturesLoadedListeners.add(listener);
1427		}
1428	}
1429
1430	private void forceCloseSocket() {
1431		if (socket != null) {
1432			try {
1433				socket.close();
1434			} catch (IOException e) {
1435				Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": io exception " + e.getMessage() + " during force close");
1436			}
1437		} else {
1438			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": socket was null during force close");
1439		}
1440	}
1441
1442	public void interrupt() {
1443		if (this.mThread != null) {
1444			this.mThread.interrupt();
1445		}
1446	}
1447
1448	public void disconnect(final boolean force) {
1449		interrupt();
1450		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force=" + Boolean.toString(force));
1451		if (force) {
1452			forceCloseSocket();
1453		} else {
1454			final TagWriter currentTagWriter = this.tagWriter;
1455			if (currentTagWriter.isActive()) {
1456				currentTagWriter.finish();
1457				final Socket currentSocket = this.socket;
1458				final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
1459				try {
1460					currentTagWriter.await(1,TimeUnit.SECONDS);
1461					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": closing stream");
1462					currentTagWriter.writeTag(Tag.end("stream:stream"));
1463					if (streamCountDownLatch != null) {
1464							if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
1465							Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": remote ended stream");
1466						} else {
1467							Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": remote has not closed socket. force closing");
1468						}
1469					}
1470				} catch (InterruptedException e) {
1471					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": interrupted while gracefully closing stream");
1472				} catch (final IOException e) {
1473					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": io exception during disconnect (" + e.getMessage() + ")");
1474				} finally {
1475					FileBackend.close(currentSocket);
1476				}
1477			} else {
1478				forceCloseSocket();
1479			}
1480		}
1481	}
1482
1483	public void resetStreamId() {
1484		this.streamId = null;
1485	}
1486
1487	private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
1488		synchronized (this.disco) {
1489			final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
1490			for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
1491				if (cursor.getValue().getFeatures().contains(feature)) {
1492					items.add(cursor);
1493				}
1494			}
1495			return items;
1496		}
1497	}
1498
1499	public Jid findDiscoItemByFeature(final String feature) {
1500		final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
1501		if (items.size() >= 1) {
1502			return items.get(0).getKey();
1503		}
1504		return null;
1505	}
1506
1507	public boolean r() {
1508		if (getFeatures().sm()) {
1509			this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1510			return true;
1511		} else {
1512			return false;
1513		}
1514	}
1515
1516	public String getMucServer() {
1517		synchronized (this.disco) {
1518			for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
1519				final ServiceDiscoveryResult value = cursor.getValue();
1520				if (value.getFeatures().contains("http://jabber.org/protocol/muc")
1521						&& !value.getFeatures().contains("jabber:iq:gateway")
1522						&& !value.hasIdentity("conference", "irc")) {
1523					return cursor.getKey().toString();
1524				}
1525			}
1526		}
1527		return null;
1528	}
1529
1530	public int getTimeToNextAttempt() {
1531		final int interval = Math.min((int) (25 * Math.pow(1.3, attempt)), 300);
1532		final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1533		return interval - secondsSinceLast;
1534	}
1535
1536	public int getAttempt() {
1537		return this.attempt;
1538	}
1539
1540	public Features getFeatures() {
1541		return this.features;
1542	}
1543
1544	public long getLastSessionEstablished() {
1545		final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1546		return System.currentTimeMillis() - diff;
1547	}
1548
1549	public long getLastConnect() {
1550		return this.lastConnect;
1551	}
1552
1553	public long getLastPingSent() {
1554		return this.lastPingSent;
1555	}
1556
1557	public long getLastDiscoStarted() {
1558		return this.lastDiscoStarted;
1559	}
1560
1561	public long getLastPacketReceived() {
1562		return this.lastPacketReceived;
1563	}
1564
1565	public void sendActive() {
1566		this.sendPacket(new ActivePacket());
1567	}
1568
1569	public void sendInactive() {
1570		this.sendPacket(new InactivePacket());
1571	}
1572
1573	public void resetAttemptCount(boolean resetConnectTime) {
1574		this.attempt = 0;
1575		if (resetConnectTime) {
1576			this.lastConnect = 0;
1577		}
1578	}
1579
1580	public void setInteractive(boolean interactive) {
1581		this.mInteractive = interactive;
1582	}
1583
1584	public Identity getServerIdentity() {
1585		synchronized (this.disco) {
1586			ServiceDiscoveryResult result = disco.get(account.getJid().toDomainJid());
1587			if (result == null) {
1588				return Identity.UNKNOWN;
1589			}
1590			for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
1591				if (id.getType().equals("im") && id.getCategory().equals("server") && id.getName() != null) {
1592					switch (id.getName()) {
1593						case "Prosody":
1594							return Identity.PROSODY;
1595						case "ejabberd":
1596							return Identity.EJABBERD;
1597						case "Slack-XMPP":
1598							return Identity.SLACK;
1599					}
1600				}
1601			}
1602		}
1603		return Identity.UNKNOWN;
1604	}
1605
1606	private IqGenerator getIqGenerator() {
1607		return mXmppConnectionService.getIqGenerator();
1608	}
1609
1610	public enum Identity {
1611		FACEBOOK,
1612		SLACK,
1613		EJABBERD,
1614		PROSODY,
1615		NIMBUZZ,
1616		UNKNOWN
1617	}
1618
1619	private static class TlsFactoryVerifier {
1620		private final SSLSocketFactory factory;
1621		private final DomainHostnameVerifier verifier;
1622
1623		public TlsFactoryVerifier(final SSLSocketFactory factory, final DomainHostnameVerifier verifier) throws IOException {
1624			this.factory = factory;
1625			this.verifier = verifier;
1626			if (factory == null || verifier == null) {
1627				throw new IOException("could not setup ssl");
1628			}
1629		}
1630	}
1631
1632	private class MyKeyManager implements X509KeyManager {
1633		@Override
1634		public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
1635			return account.getPrivateKeyAlias();
1636		}
1637
1638		@Override
1639		public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
1640			return null;
1641		}
1642
1643		@Override
1644		public X509Certificate[] getCertificateChain(String alias) {
1645			Log.d(Config.LOGTAG, "getting certificate chain");
1646			try {
1647				return KeyChain.getCertificateChain(mXmppConnectionService, alias);
1648			} catch (Exception e) {
1649				Log.d(Config.LOGTAG, e.getMessage());
1650				return new X509Certificate[0];
1651			}
1652		}
1653
1654		@Override
1655		public String[] getClientAliases(String s, Principal[] principals) {
1656			final String alias = account.getPrivateKeyAlias();
1657			return alias != null ? new String[]{alias} : new String[0];
1658		}
1659
1660		@Override
1661		public String[] getServerAliases(String s, Principal[] principals) {
1662			return new String[0];
1663		}
1664
1665		@Override
1666		public PrivateKey getPrivateKey(String alias) {
1667			try {
1668				return KeyChain.getPrivateKey(mXmppConnectionService, alias);
1669			} catch (Exception e) {
1670				return null;
1671			}
1672		}
1673	}
1674
1675	private class StateChangingError extends Error {
1676		private final Account.State state;
1677
1678		public StateChangingError(Account.State state) {
1679			this.state = state;
1680		}
1681	}
1682
1683	private class StateChangingException extends IOException {
1684		private final Account.State state;
1685
1686		public StateChangingException(Account.State state) {
1687			this.state = state;
1688		}
1689	}
1690
1691	public class Features {
1692		XmppConnection connection;
1693		private boolean carbonsEnabled = false;
1694		private boolean encryptionEnabled = false;
1695		private boolean blockListRequested = false;
1696
1697		public Features(final XmppConnection connection) {
1698			this.connection = connection;
1699		}
1700
1701		private boolean hasDiscoFeature(final Jid server, final String feature) {
1702			synchronized (XmppConnection.this.disco) {
1703				return connection.disco.containsKey(server) &&
1704						connection.disco.get(server).getFeatures().contains(feature);
1705			}
1706		}
1707
1708		public boolean carbons() {
1709			return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1710		}
1711
1712		public boolean blocking() {
1713			return hasDiscoFeature(account.getServer(), Namespace.BLOCKING);
1714		}
1715
1716		public boolean spamReporting() {
1717			return hasDiscoFeature(account.getServer(), "urn:xmpp:reporting:reason:spam:0");
1718		}
1719
1720		public boolean flexibleOfflineMessageRetrieval() {
1721			return hasDiscoFeature(account.getServer(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
1722		}
1723
1724		public boolean register() {
1725			return hasDiscoFeature(account.getServer(), Namespace.REGISTER);
1726		}
1727
1728		public boolean sm() {
1729			return streamId != null
1730					|| (connection.streamFeatures != null && connection.streamFeatures.hasChild("sm"));
1731		}
1732
1733		public boolean csi() {
1734			return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
1735		}
1736
1737		public boolean pep() {
1738			synchronized (XmppConnection.this.disco) {
1739				ServiceDiscoveryResult info = disco.get(account.getJid().toBareJid());
1740				return info != null && info.hasIdentity("pubsub", "pep");
1741			}
1742		}
1743
1744		public boolean pepPersistent() {
1745			synchronized (XmppConnection.this.disco) {
1746				ServiceDiscoveryResult info = disco.get(account.getJid().toBareJid());
1747				return info != null && info.getFeatures().contains("http://jabber.org/protocol/pubsub#persistent-items");
1748			}
1749		}
1750
1751		public boolean pepPublishOptions() {
1752			return hasDiscoFeature(account.getJid().toBareJid(),Namespace.PUBSUB_PUBLISH_OPTIONS);
1753		}
1754
1755		public boolean pepOmemoWhitelisted() {
1756			return hasDiscoFeature(account.getJid().toBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
1757		}
1758
1759		public boolean mam() {
1760			return hasDiscoFeature(account.getJid().toBareJid(), Namespace.MAM)
1761					|| hasDiscoFeature(account.getJid().toBareJid(), Namespace.MAM_LEGACY);
1762		}
1763
1764		public boolean mamLegacy() {
1765			return !hasDiscoFeature(account.getJid().toBareJid(), Namespace.MAM)
1766					&& hasDiscoFeature(account.getJid().toBareJid(), Namespace.MAM_LEGACY);
1767		}
1768
1769		public boolean push() {
1770			return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:push:0")
1771					|| hasDiscoFeature(account.getServer(), "urn:xmpp:push:0");
1772		}
1773
1774		public boolean rosterVersioning() {
1775			return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
1776		}
1777
1778		public void setBlockListRequested(boolean value) {
1779			this.blockListRequested = value;
1780		}
1781
1782		public boolean httpUpload(long filesize) {
1783			if (Config.DISABLE_HTTP_UPLOAD) {
1784				return false;
1785			} else {
1786				List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Namespace.HTTP_UPLOAD);
1787				if (items.size() > 0) {
1788					try {
1789						long maxsize = Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Namespace.HTTP_UPLOAD, "max-file-size"));
1790						if (filesize <= maxsize) {
1791							return true;
1792						} else {
1793							Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": http upload is not available for files with size " + filesize + " (max is " + maxsize + ")");
1794							return false;
1795						}
1796					} catch (Exception e) {
1797						return true;
1798					}
1799				} else {
1800					return false;
1801				}
1802			}
1803		}
1804
1805		public long getMaxHttpUploadSize() {
1806			List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Namespace.HTTP_UPLOAD);
1807			if (items.size() > 0) {
1808				try {
1809					return Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Namespace.HTTP_UPLOAD, "max-file-size"));
1810				} catch (Exception e) {
1811					return -1;
1812				}
1813			} else {
1814				return -1;
1815			}
1816		}
1817
1818		public boolean stanzaIds() {
1819			return hasDiscoFeature(account.getJid().toBareJid(), Namespace.STANZA_IDS);
1820		}
1821	}
1822}