XmppConnection.java

   1package eu.siacs.conversations.xmpp;
   2
   3import android.graphics.Bitmap;
   4import android.graphics.BitmapFactory;
   5import android.os.Bundle;
   6import android.os.Parcelable;
   7import android.os.PowerManager;
   8import android.os.PowerManager.WakeLock;
   9import android.os.SystemClock;
  10import android.security.KeyChain;
  11import android.util.Base64;
  12import android.util.Log;
  13import android.util.Pair;
  14import android.util.SparseArray;
  15
  16import org.json.JSONException;
  17import org.json.JSONObject;
  18import org.xmlpull.v1.XmlPullParserException;
  19
  20import java.io.ByteArrayInputStream;
  21import java.io.IOException;
  22import java.io.InputStream;
  23import java.math.BigInteger;
  24import java.net.ConnectException;
  25import java.net.IDN;
  26import java.net.InetAddress;
  27import java.net.InetSocketAddress;
  28import java.net.Socket;
  29import java.net.UnknownHostException;
  30import java.net.URL;
  31import java.security.KeyManagementException;
  32import java.security.NoSuchAlgorithmException;
  33import java.security.Principal;
  34import java.security.PrivateKey;
  35import java.security.cert.X509Certificate;
  36import java.util.ArrayList;
  37import java.util.HashMap;
  38import java.util.Hashtable;
  39import java.util.Iterator;
  40import java.util.List;
  41import java.util.Map.Entry;
  42
  43import javax.net.ssl.HostnameVerifier;
  44import javax.net.ssl.KeyManager;
  45import javax.net.ssl.SSLContext;
  46import javax.net.ssl.SSLSocket;
  47import javax.net.ssl.SSLSocketFactory;
  48import javax.net.ssl.X509KeyManager;
  49import javax.net.ssl.X509TrustManager;
  50
  51import de.duenndns.ssl.MemorizingTrustManager;
  52import eu.siacs.conversations.Config;
  53import eu.siacs.conversations.crypto.XmppDomainVerifier;
  54import eu.siacs.conversations.crypto.sasl.DigestMd5;
  55import eu.siacs.conversations.crypto.sasl.External;
  56import eu.siacs.conversations.crypto.sasl.Plain;
  57import eu.siacs.conversations.crypto.sasl.SaslMechanism;
  58import eu.siacs.conversations.crypto.sasl.ScramSha1;
  59import eu.siacs.conversations.entities.Account;
  60import eu.siacs.conversations.entities.Message;
  61import eu.siacs.conversations.generator.IqGenerator;
  62import eu.siacs.conversations.services.XmppConnectionService;
  63import eu.siacs.conversations.utils.CryptoHelper;
  64import eu.siacs.conversations.utils.DNSHelper;
  65import eu.siacs.conversations.utils.SSLSocketHelper;
  66import eu.siacs.conversations.utils.SocksSocketFactory;
  67import eu.siacs.conversations.utils.Xmlns;
  68import eu.siacs.conversations.xml.Element;
  69import eu.siacs.conversations.xml.Tag;
  70import eu.siacs.conversations.xml.TagWriter;
  71import eu.siacs.conversations.xml.XmlReader;
  72import eu.siacs.conversations.xmpp.forms.Data;
  73import eu.siacs.conversations.xmpp.forms.Field;
  74import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  75import eu.siacs.conversations.xmpp.jid.Jid;
  76import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  77import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  78import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza;
  79import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
  80import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  81import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  82import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  83import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
  84import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
  85import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
  86import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
  87import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
  88import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
  89
  90public class XmppConnection implements Runnable {
  91
  92	private static final int PACKET_IQ = 0;
  93	private static final int PACKET_MESSAGE = 1;
  94	private static final int PACKET_PRESENCE = 2;
  95	protected Account account;
  96	private final WakeLock wakeLock;
  97	private Socket socket;
  98	private XmlReader tagReader;
  99	private TagWriter tagWriter;
 100	private final Features features = new Features(this);
 101	private boolean needsBinding = true;
 102	private boolean shouldAuthenticate = true;
 103	private Element streamFeatures;
 104	private final HashMap<Jid, Info> disco = new HashMap<>();
 105
 106	private String streamId = null;
 107	private int smVersion = 3;
 108	private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
 109
 110	private int stanzasReceived = 0;
 111	private int stanzasSent = 0;
 112	private long lastPacketReceived = 0;
 113	private long lastPingSent = 0;
 114	private long lastConnect = 0;
 115	private long lastSessionStarted = 0;
 116	private long lastDiscoStarted = 0;
 117	private int mPendingServiceDiscoveries = 0;
 118	private final ArrayList<String> mPendingServiceDiscoveriesIds = new ArrayList<>();
 119	private boolean mInteractive = false;
 120	private int attempt = 0;
 121	private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks = new Hashtable<>();
 122	private OnPresencePacketReceived presenceListener = null;
 123	private OnJinglePacketReceived jingleListener = null;
 124	private OnIqPacketReceived unregisteredIqListener = null;
 125	private OnMessagePacketReceived messageListener = null;
 126	private OnStatusChanged statusListener = null;
 127	private OnBindListener bindListener = null;
 128	private final ArrayList<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners = new ArrayList<>();
 129	private OnMessageAcknowledged acknowledgedListener = null;
 130	private XmppConnectionService mXmppConnectionService = null;
 131
 132	private SaslMechanism saslMechanism;
 133
 134	private X509KeyManager mKeyManager = new X509KeyManager() {
 135		@Override
 136		public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
 137			return account.getPrivateKeyAlias();
 138		}
 139
 140		@Override
 141		public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
 142			return null;
 143		}
 144
 145		@Override
 146		public X509Certificate[] getCertificateChain(String alias) {
 147			try {
 148				return KeyChain.getCertificateChain(mXmppConnectionService, alias);
 149			} catch (Exception e) {
 150				return new X509Certificate[0];
 151			}
 152		}
 153
 154		@Override
 155		public String[] getClientAliases(String s, Principal[] principals) {
 156			return new String[0];
 157		}
 158
 159		@Override
 160		public String[] getServerAliases(String s, Principal[] principals) {
 161			return new String[0];
 162		}
 163
 164		@Override
 165		public PrivateKey getPrivateKey(String alias) {
 166			try {
 167				return KeyChain.getPrivateKey(mXmppConnectionService, alias);
 168			} catch (Exception e) {
 169				return null;
 170			}
 171		}
 172	};
 173	private Identity mServerIdentity = Identity.UNKNOWN;
 174
 175	private OnIqPacketReceived createPacketReceiveHandler() {
 176		return new OnIqPacketReceived() {
 177			@Override
 178			public void onIqPacketReceived(Account account, IqPacket packet) {
 179				if (packet.getType() == IqPacket.TYPE.RESULT) {
 180					account.setOption(Account.OPTION_REGISTER,
 181							false);
 182					changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
 183				} else if (packet.hasChild("error")
 184						&& (packet.findChild("error")
 185						.hasChild("conflict"))) {
 186					changeStatus(Account.State.REGISTRATION_CONFLICT);
 187				} else {
 188					changeStatus(Account.State.REGISTRATION_FAILED);
 189					Log.d(Config.LOGTAG, packet.toString());
 190				}
 191				disconnect(true);
 192			}
 193		};
 194	}
 195
 196	public XmppConnection(final Account account, final XmppConnectionService service) {
 197		this.account = account;
 198		this.wakeLock = service.getPowerManager().newWakeLock(
 199				PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString());
 200		tagWriter = new TagWriter();
 201		mXmppConnectionService = service;
 202	}
 203
 204	protected void changeStatus(final Account.State nextStatus) {
 205		if (account.getStatus() != nextStatus) {
 206			if ((nextStatus == Account.State.OFFLINE)
 207					&& (account.getStatus() != Account.State.CONNECTING)
 208					&& (account.getStatus() != Account.State.ONLINE)
 209					&& (account.getStatus() != Account.State.DISABLED)) {
 210				return;
 211					}
 212			if (nextStatus == Account.State.ONLINE) {
 213				this.attempt = 0;
 214			}
 215			account.setStatus(nextStatus);
 216			if (statusListener != null) {
 217				statusListener.onStatusChanged(account);
 218			}
 219		}
 220	}
 221
 222	protected void connect() {
 223		Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting");
 224		features.encryptionEnabled = false;
 225		lastConnect = SystemClock.elapsedRealtime();
 226		lastPingSent = SystemClock.elapsedRealtime();
 227		lastDiscoStarted = Long.MAX_VALUE;
 228		this.attempt++;
 229		if (account.getJid().getDomainpart().equals("chat.facebook.com")) {
 230			mServerIdentity = Identity.FACEBOOK;
 231		}
 232		try {
 233			shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER);
 234			tagReader = new XmlReader(wakeLock);
 235			tagWriter = new TagWriter();
 236			this.changeStatus(Account.State.CONNECTING);
 237			final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion();
 238			if (useTor) {
 239				String destination;
 240				if (account.getHostname() == null || account.getHostname().isEmpty()) {
 241					destination = account.getServer().toString();
 242				} else {
 243					destination = account.getHostname();
 244				}
 245				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": connect to "+destination+" via TOR");
 246				socket = SocksSocketFactory.createSocketOverTor(destination,account.getPort());
 247				startXmpp();
 248			} else if (DNSHelper.isIp(account.getServer().toString())) {
 249				socket = new Socket();
 250				try {
 251					socket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000);
 252				} catch (IOException e) {
 253					throw new UnknownHostException();
 254				}
 255				startXmpp();
 256			} else {
 257				final Bundle result = DNSHelper.getSRVRecord(account.getServer(), mXmppConnectionService);
 258				final ArrayList<Parcelable>values = result.getParcelableArrayList("values");
 259				for(Iterator<Parcelable> iterator = values.iterator(); iterator.hasNext();) {
 260					final Bundle namePort = (Bundle) iterator.next();
 261					try {
 262						String srvRecordServer;
 263						try {
 264							srvRecordServer = IDN.toASCII(namePort.getString("name"));
 265						} catch (final IllegalArgumentException e) {
 266							// TODO: Handle me?`
 267							srvRecordServer = "";
 268						}
 269						final int srvRecordPort = namePort.getInt("port");
 270						final String srvIpServer = namePort.getString("ip");
 271						// if tls is true, encryption is implied and must not be started
 272						features.encryptionEnabled = namePort.getBoolean("tls");
 273						final InetSocketAddress addr;
 274						if (srvIpServer != null) {
 275							addr = new InetSocketAddress(srvIpServer, srvRecordPort);
 276							Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
 277									+ ": using values from dns " + srvRecordServer
 278									+ "[" + srvIpServer + "]:" + srvRecordPort + " tls: " + features.encryptionEnabled);
 279						} else {
 280							addr = new InetSocketAddress(srvRecordServer, srvRecordPort);
 281							Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
 282									+ ": using values from dns "
 283									+ srvRecordServer + ":" + srvRecordPort + " tls: " + features.encryptionEnabled);
 284						}
 285
 286						if (!features.encryptionEnabled) {
 287							socket = new Socket();
 288							socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
 289						} else {
 290							final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
 291							socket = tlsFactoryVerifier.factory.createSocket();
 292
 293							if (socket == null) {
 294								throw new IOException("could not initialize ssl socket");
 295							}
 296
 297							SSLSocketHelper.setSecurity((SSLSocket) socket);
 298							SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart());
 299							SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client");
 300
 301							socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
 302
 303							if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) {
 304								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
 305								throw new SecurityException();
 306							}
 307						}
 308
 309						if(startXmpp())
 310							break; // successfully connected to server that speaks xmpp
 311					} catch (final Throwable e) {
 312						Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() +"("+e.getClass().getName()+")");
 313						if (!iterator.hasNext()) {
 314							throw new UnknownHostException();
 315						}
 316					}
 317				}
 318			}
 319			processStream();
 320		} catch (final IncompatibleServerException e) {
 321			this.changeStatus(Account.State.INCOMPATIBLE_SERVER);
 322		} catch (final SecurityException e) {
 323			this.changeStatus(Account.State.SECURITY_ERROR);
 324		} catch (final UnauthorizedException e) {
 325			this.changeStatus(Account.State.UNAUTHORIZED);
 326		} catch (final UnknownHostException | ConnectException e) {
 327			this.changeStatus(Account.State.SERVER_NOT_FOUND);
 328		} catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
 329			this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
 330		} catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) {
 331			Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
 332			this.changeStatus(Account.State.OFFLINE);
 333			this.attempt--; //don't count attempt when reconnecting instantly anyway
 334		} finally {
 335			if (socket != null) {
 336				try {
 337					socket.close();
 338				} catch (IOException e) {
 339
 340				}
 341			}
 342			if (wakeLock.isHeld()) {
 343				try {
 344					wakeLock.release();
 345				} catch (final RuntimeException ignored) {
 346				}
 347			}
 348		}
 349	}
 350
 351	/**
 352	 * Starts xmpp protocol, call after connecting to socket
 353	 * @return true if server returns with valid xmpp, false otherwise
 354	 * @throws IOException Unknown tag on connect
 355	 * @throws XmlPullParserException Bad Xml
 356	 * @throws NoSuchAlgorithmException Other error
 357     */
 358	private boolean startXmpp() throws IOException, XmlPullParserException, NoSuchAlgorithmException {
 359		tagWriter.setOutputStream(socket.getOutputStream());
 360		tagReader.setInputStream(socket.getInputStream());
 361		tagWriter.beginDocument();
 362		sendStartStream();
 363		Tag nextTag;
 364		while ((nextTag = tagReader.readTag()) != null) {
 365			if (nextTag.isStart("stream")) {
 366				return true;
 367			} else {
 368				throw new IOException("unknown tag on connect");
 369			}
 370		}
 371		if (socket.isConnected()) {
 372			socket.close();
 373		}
 374		return false;
 375	}
 376
 377	private static class TlsFactoryVerifier {
 378		private final SSLSocketFactory factory;
 379		private final HostnameVerifier verifier;
 380
 381		public TlsFactoryVerifier(final SSLSocketFactory factory, final HostnameVerifier verifier) throws IOException {
 382			this.factory = factory;
 383			this.verifier = verifier;
 384			if (factory == null || verifier == null) {
 385				throw new IOException("could not setup ssl");
 386			}
 387		}
 388	}
 389
 390	private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException {
 391		final SSLContext sc = SSLContext.getInstance("TLS");
 392		MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager();
 393		KeyManager[] keyManager;
 394		if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) {
 395			keyManager = new KeyManager[]{mKeyManager};
 396		} else {
 397			keyManager = null;
 398		}
 399		sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, mXmppConnectionService.getRNG());
 400		final SSLSocketFactory factory = sc.getSocketFactory();
 401		final HostnameVerifier verifier;
 402		if (mInteractive) {
 403			verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier());
 404		} else {
 405			verifier = trustManager.wrapHostnameVerifierNonInteractive(new XmppDomainVerifier());
 406		}
 407
 408		return new TlsFactoryVerifier(factory, verifier);
 409	}
 410
 411	@Override
 412	public void run() {
 413		try {
 414			if (socket != null) {
 415				socket.close();
 416			}
 417		} catch (final IOException ignored) {
 418
 419		}
 420		connect();
 421	}
 422
 423	private void processStream() throws XmlPullParserException, IOException, NoSuchAlgorithmException {
 424		Tag nextTag = tagReader.readTag();
 425		while (nextTag != null && !nextTag.isEnd("stream")) {
 426			if (nextTag.isStart("error")) {
 427				processStreamError(nextTag);
 428			} else if (nextTag.isStart("features")) {
 429				processStreamFeatures(nextTag);
 430			} else if (nextTag.isStart("proceed")) {
 431				switchOverToTls(nextTag);
 432			} else if (nextTag.isStart("success")) {
 433				final String challenge = tagReader.readElement(nextTag).getContent();
 434				try {
 435					saslMechanism.getResponse(challenge);
 436				} catch (final SaslMechanism.AuthenticationException e) {
 437					disconnect(true);
 438					Log.e(Config.LOGTAG, String.valueOf(e));
 439				}
 440				Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in");
 441				account.setKey(Account.PINNED_MECHANISM_KEY,
 442						String.valueOf(saslMechanism.getPriority()));
 443				tagReader.reset();
 444				sendStartStream();
 445				final Tag tag = tagReader.readTag();
 446				if (tag != null && tag.isStart("stream")) {
 447					processStream();
 448				} else {
 449					throw new IOException("server didn't restart stream after successful auth");
 450				}
 451				break;
 452			} else if (nextTag.isStart("failure")) {
 453				throw new UnauthorizedException();
 454			} else if (nextTag.isStart("challenge")) {
 455				final String challenge = tagReader.readElement(nextTag).getContent();
 456				final Element response = new Element("response");
 457				response.setAttribute("xmlns",
 458						"urn:ietf:params:xml:ns:xmpp-sasl");
 459				try {
 460					response.setContent(saslMechanism.getResponse(challenge));
 461				} catch (final SaslMechanism.AuthenticationException e) {
 462					// TODO: Send auth abort tag.
 463					Log.e(Config.LOGTAG, e.toString());
 464				}
 465				tagWriter.writeElement(response);
 466			} else if (nextTag.isStart("enabled")) {
 467				final Element enabled = tagReader.readElement(nextTag);
 468				if ("true".equals(enabled.getAttribute("resume"))) {
 469					this.streamId = enabled.getAttribute("id");
 470					Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
 471							+ ": stream managment(" + smVersion
 472							+ ") enabled (resumable)");
 473				} else {
 474					Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
 475							+ ": stream management(" + smVersion + ") enabled");
 476				}
 477				this.stanzasReceived = 0;
 478				final RequestPacket r = new RequestPacket(smVersion);
 479				tagWriter.writeStanzaAsync(r);
 480			} else if (nextTag.isStart("resumed")) {
 481				lastPacketReceived = SystemClock.elapsedRealtime();
 482				final Element resumed = tagReader.readElement(nextTag);
 483				final String h = resumed.getAttribute("h");
 484				try {
 485					final int serverCount = Integer.parseInt(h);
 486					if (serverCount != stanzasSent) {
 487						Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
 488								+ ": session resumed with lost packages");
 489						stanzasSent = serverCount;
 490					} else {
 491						Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": session resumed");
 492					}
 493					acknowledgeStanzaUpTo(serverCount);
 494					ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
 495					for(int i = 0; i < this.mStanzaQueue.size(); ++i) {
 496						failedStanzas.add(mStanzaQueue.valueAt(i));
 497					}
 498					mStanzaQueue.clear();
 499					Log.d(Config.LOGTAG,"resending "+failedStanzas.size()+" stanzas");
 500					for(AbstractAcknowledgeableStanza packet : failedStanzas) {
 501						if (packet instanceof MessagePacket) {
 502							MessagePacket message = (MessagePacket) packet;
 503							mXmppConnectionService.markMessage(account,
 504									message.getTo().toBareJid(),
 505									message.getId(),
 506									Message.STATUS_UNSEND);
 507						}
 508						sendPacket(packet);
 509					}
 510				} catch (final NumberFormatException ignored) {
 511				}
 512				Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": online with resource " + account.getResource());
 513				changeStatus(Account.State.ONLINE);
 514			} else if (nextTag.isStart("r")) {
 515				tagReader.readElement(nextTag);
 516				if (Config.EXTENDED_SM_LOGGING) {
 517					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": acknowledging stanza #" + this.stanzasReceived);
 518				}
 519				final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
 520				tagWriter.writeStanzaAsync(ack);
 521			} else if (nextTag.isStart("a")) {
 522				final Element ack = tagReader.readElement(nextTag);
 523				lastPacketReceived = SystemClock.elapsedRealtime();
 524				try {
 525					final int serverSequence = Integer.parseInt(ack.getAttribute("h"));
 526					acknowledgeStanzaUpTo(serverSequence);
 527				} catch (NumberFormatException e) {
 528					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server send ack without sequence number");
 529				}
 530			} else if (nextTag.isStart("failed")) {
 531				tagReader.readElement(nextTag);
 532				Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": resumption failed");
 533				streamId = null;
 534				if (account.getStatus() != Account.State.ONLINE) {
 535					sendBindRequest();
 536				}
 537			} else if (nextTag.isStart("iq")) {
 538				processIq(nextTag);
 539			} else if (nextTag.isStart("message")) {
 540				processMessage(nextTag);
 541			} else if (nextTag.isStart("presence")) {
 542				processPresence(nextTag);
 543			}
 544			nextTag = tagReader.readTag();
 545		}
 546		throw new IOException("reached end of stream. last tag was "+nextTag);
 547	}
 548
 549	private void acknowledgeStanzaUpTo(int serverCount) {
 550		for (int i = 0; i < mStanzaQueue.size(); ++i) {
 551			if (serverCount >= mStanzaQueue.keyAt(i)) {
 552				if (Config.EXTENDED_SM_LOGGING) {
 553					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i));
 554				}
 555				AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
 556				if (stanza instanceof MessagePacket && acknowledgedListener != null) {
 557					MessagePacket packet = (MessagePacket) stanza;
 558					acknowledgedListener.onMessageAcknowledged(account, packet.getId());
 559				}
 560				mStanzaQueue.removeAt(i);
 561				i--;
 562			}
 563		}
 564	}
 565
 566	private Element processPacket(final Tag currentTag, final int packetType)
 567		throws XmlPullParserException, IOException {
 568		Element element;
 569		switch (packetType) {
 570			case PACKET_IQ:
 571				element = new IqPacket();
 572				break;
 573			case PACKET_MESSAGE:
 574				element = new MessagePacket();
 575				break;
 576			case PACKET_PRESENCE:
 577				element = new PresencePacket();
 578				break;
 579			default:
 580				return null;
 581		}
 582		element.setAttributes(currentTag.getAttributes());
 583		Tag nextTag = tagReader.readTag();
 584		if (nextTag == null) {
 585			throw new IOException("interrupted mid tag");
 586		}
 587		while (!nextTag.isEnd(element.getName())) {
 588			if (!nextTag.isNo()) {
 589				final Element child = tagReader.readElement(nextTag);
 590				final String type = currentTag.getAttribute("type");
 591				if (packetType == PACKET_IQ
 592						&& "jingle".equals(child.getName())
 593						&& ("set".equalsIgnoreCase(type) || "get"
 594							.equalsIgnoreCase(type))) {
 595					element = new JinglePacket();
 596					element.setAttributes(currentTag.getAttributes());
 597							}
 598				element.addChild(child);
 599			}
 600			nextTag = tagReader.readTag();
 601			if (nextTag == null) {
 602				throw new IOException("interrupted mid tag");
 603			}
 604		}
 605		if (stanzasReceived == Integer.MAX_VALUE) {
 606			resetStreamId();
 607			throw new IOException("time to restart the session. cant handle >2 billion pcks");
 608		}
 609		++stanzasReceived;
 610		lastPacketReceived = SystemClock.elapsedRealtime();
 611		return element;
 612	}
 613
 614	private void processIq(final Tag currentTag) throws XmlPullParserException, IOException {
 615		final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
 616
 617		if (packet.getId() == null) {
 618			return; // an iq packet without id is definitely invalid
 619		}
 620
 621		if (packet instanceof JinglePacket) {
 622			if (this.jingleListener != null) {
 623				this.jingleListener.onJinglePacketReceived(account,(JinglePacket) packet);
 624			}
 625		} else {
 626			OnIqPacketReceived callback = null;
 627			synchronized (this.packetCallbacks) {
 628				if (packetCallbacks.containsKey(packet.getId())) {
 629					final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId());
 630					// Packets to the server should have responses from the server
 631					if (packetCallbackDuple.first.toServer(account)) {
 632						if (packet.fromServer(account) || mServerIdentity == Identity.FACEBOOK) {
 633							callback = packetCallbackDuple.second;
 634							packetCallbacks.remove(packet.getId());
 635						} else {
 636							Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
 637						}
 638					} else {
 639						if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
 640							callback = packetCallbackDuple.second;
 641							packetCallbacks.remove(packet.getId());
 642						} else {
 643							Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
 644						}
 645					}
 646				} else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
 647					callback = this.unregisteredIqListener;
 648				}
 649			}
 650			if (callback != null) {
 651				callback.onIqPacketReceived(account,packet);
 652			}
 653		}
 654	}
 655
 656	private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException {
 657		final MessagePacket packet = (MessagePacket) processPacket(currentTag,PACKET_MESSAGE);
 658		this.messageListener.onMessagePacketReceived(account, packet);
 659	}
 660
 661	private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException {
 662		PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
 663		this.presenceListener.onPresencePacketReceived(account, packet);
 664	}
 665
 666	private void sendStartTLS() throws IOException {
 667		final Tag startTLS = Tag.empty("starttls");
 668		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
 669		tagWriter.writeTag(startTLS);
 670	}
 671
 672
 673
 674	private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
 675		tagReader.readTag();
 676		try {
 677			final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
 678			final InetAddress address = socket == null ? null : socket.getInetAddress();
 679
 680			if (address == null) {
 681				throw new IOException("could not setup ssl");
 682			}
 683
 684			final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
 685
 686			if (sslSocket == null) {
 687				throw new IOException("could not initialize ssl socket");
 688			}
 689
 690			SSLSocketHelper.setSecurity(sslSocket);
 691
 692			if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), sslSocket.getSession())) {
 693				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed");
 694				throw new SecurityException();
 695			}
 696			tagReader.setInputStream(sslSocket.getInputStream());
 697			tagWriter.setOutputStream(sslSocket.getOutputStream());
 698			sendStartStream();
 699			Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": TLS connection established");
 700			features.encryptionEnabled = true;
 701			final Tag tag = tagReader.readTag();
 702			if (tag != null && tag.isStart("stream")) {
 703				processStream();
 704			} else {
 705				throw new IOException("server didn't restart stream after STARTTLS");
 706			}
 707			sslSocket.close();
 708		} catch (final NoSuchAlgorithmException | KeyManagementException e1) {
 709			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
 710			throw new SecurityException();
 711		}
 712	}
 713
 714	private void processStreamFeatures(final Tag currentTag)
 715		throws XmlPullParserException, IOException {
 716		this.streamFeatures = tagReader.readElement(currentTag);
 717		if (this.streamFeatures.hasChild("starttls") && !features.encryptionEnabled) {
 718			sendStartTLS();
 719		} else if (this.streamFeatures.hasChild("register")
 720				&& account.isOptionSet(Account.OPTION_REGISTER)
 721				&& features.encryptionEnabled) {
 722			sendRegistryRequest();
 723		} else if (!this.streamFeatures.hasChild("register")
 724				&& account.isOptionSet(Account.OPTION_REGISTER)) {
 725			changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
 726			disconnect(true);
 727		} else if (this.streamFeatures.hasChild("mechanisms")
 728				&& shouldAuthenticate && features.encryptionEnabled) {
 729			final List<String> mechanisms = extractMechanisms(streamFeatures
 730					.findChild("mechanisms"));
 731			final Element auth = new Element("auth");
 732			auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
 733			if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) {
 734				saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
 735			} else if (mechanisms.contains("SCRAM-SHA-1")) {
 736				saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
 737			} else if (mechanisms.contains("PLAIN")) {
 738				saslMechanism = new Plain(tagWriter, account);
 739			} else if (mechanisms.contains("DIGEST-MD5")) {
 740				saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
 741			}
 742			if (saslMechanism != null) {
 743				final JSONObject keys = account.getKeys();
 744				try {
 745					if (keys.has(Account.PINNED_MECHANISM_KEY) &&
 746							keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority()) {
 747						Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() +
 748								" has lower priority (" + String.valueOf(saslMechanism.getPriority()) +
 749								") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) +
 750								"). Possible downgrade attack?");
 751						throw new SecurityException();
 752					}
 753				} catch (final JSONException e) {
 754					Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism");
 755				}
 756				Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with " + saslMechanism.getMechanism());
 757				auth.setAttribute("mechanism", saslMechanism.getMechanism());
 758				if (!saslMechanism.getClientFirstMessage().isEmpty()) {
 759					auth.setContent(saslMechanism.getClientFirstMessage());
 760				}
 761				tagWriter.writeElement(auth);
 762			} else {
 763				throw new IncompatibleServerException();
 764			}
 765		} else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) {
 766			if (Config.EXTENDED_SM_LOGGING) {
 767				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": resuming after stanza #"+stanzasReceived);
 768			}
 769			final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion);
 770			this.tagWriter.writeStanzaAsync(resume);
 771		} else if (needsBinding) {
 772			if (this.streamFeatures.hasChild("bind")) {
 773				sendBindRequest();
 774			} else {
 775				throw new IncompatibleServerException();
 776			}
 777		}
 778	}
 779
 780	private List<String> extractMechanisms(final Element stream) {
 781		final ArrayList<String> mechanisms = new ArrayList<>(stream
 782				.getChildren().size());
 783		for (final Element child : stream.getChildren()) {
 784			mechanisms.add(child.getContent());
 785		}
 786		return mechanisms;
 787	}
 788
 789	public void sendCaptchaRegistryRequest(String id, Data data) {
 790		if (data == null) {
 791			setAccountCreationFailed("");
 792		} else {
 793			IqPacket request = getIqGenerator().generateCreateAccountWithCaptcha(account, id, data);
 794			sendIqPacket(request, createPacketReceiveHandler());
 795		}
 796	}
 797
 798	private void sendRegistryRequest() {
 799		final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
 800		register.query("jabber:iq:register");
 801		register.setTo(account.getServer());
 802		sendIqPacket(register, new OnIqPacketReceived() {
 803
 804			@Override
 805			public void onIqPacketReceived(final Account account, final IqPacket packet) {
 806				boolean failed = false;
 807				if (packet.getType() == IqPacket.TYPE.RESULT
 808						&& packet.query().hasChild("username")
 809						&& (packet.query().hasChild("password"))) {
 810					final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
 811					final Element username = new Element("username").setContent(account.getUsername());
 812					final Element password = new Element("password").setContent(account.getPassword());
 813					register.query("jabber:iq:register").addChild(username);
 814					register.query().addChild(password);
 815					sendIqPacket(register, createPacketReceiveHandler());
 816				} else if (packet.getType() == IqPacket.TYPE.RESULT
 817						&& (packet.query().hasChild("x", "jabber:x:data"))) {
 818					final Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
 819					final Element blob = packet.query().findChild("data", "urn:xmpp:bob");
 820					final String id = packet.getId();
 821
 822					Bitmap captcha = null;
 823					if (blob != null) {
 824						try {
 825							final String base64Blob = blob.getContent();
 826							final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
 827							InputStream stream = new ByteArrayInputStream(strBlob);
 828							captcha = BitmapFactory.decodeStream(stream);
 829						} catch (Exception e) {
 830							//ignored
 831						}
 832					} else {
 833						try {
 834							Field url = data.getFieldByName("url");
 835							String urlString = url.findChildContent("value");
 836							URL uri = new URL(urlString);
 837							captcha = BitmapFactory.decodeStream(uri.openConnection().getInputStream());
 838						} catch (IOException e) {
 839							Log.e(Config.LOGTAG, e.toString());
 840						}
 841					}
 842
 843					if (captcha != null) {
 844						failed = !mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha);
 845					}
 846				} else {
 847					failed = true;
 848				}
 849
 850				if (failed) {
 851					final Element instructions = packet.query().findChild("instructions");
 852					setAccountCreationFailed((instructions != null) ? instructions.getContent() : "");
 853				}
 854			}
 855		});
 856	}
 857
 858	private void setAccountCreationFailed(String instructions) {
 859		changeStatus(Account.State.REGISTRATION_FAILED);
 860		disconnect(true);
 861		Log.d(Config.LOGTAG, account.getJid().toBareJid()
 862				+ ": could not register. instructions are"
 863				+ instructions);
 864	}
 865
 866	private void sendBindRequest() {
 867		while(!mXmppConnectionService.areMessagesInitialized() && socket != null && !socket.isClosed()) {
 868			try {
 869				Thread.sleep(500);
 870			} catch (final InterruptedException ignored) {
 871			}
 872		}
 873		needsBinding = false;
 874		clearIqCallbacks();
 875		final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
 876		iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
 877				.addChild("resource").setContent(account.getResource());
 878		this.sendUnmodifiedIqPacket(iq, new OnIqPacketReceived() {
 879			@Override
 880			public void onIqPacketReceived(final Account account, final IqPacket packet) {
 881				if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
 882					return;
 883				}
 884				final Element bind = packet.findChild("bind");
 885				if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
 886					final Element jid = bind.findChild("jid");
 887					if (jid != null && jid.getContent() != null) {
 888						try {
 889							account.setResource(Jid.fromString(jid.getContent()).getResourcepart());
 890						} catch (final InvalidJidException e) {
 891							// TODO: Handle the case where an external JID is technically invalid?
 892						}
 893						if (streamFeatures.hasChild("session")) {
 894							sendStartSession();
 895						} else {
 896							sendPostBindInitialization();
 897						}
 898					} else {
 899						Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure");
 900						disconnect(true);
 901					}
 902				} else {
 903					Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure");
 904					disconnect(true);
 905				}
 906			}
 907		});
 908	}
 909
 910	private void clearIqCallbacks() {
 911		final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
 912		final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
 913		synchronized (this.packetCallbacks) {
 914			if (this.packetCallbacks.size() == 0) {
 915				return;
 916			}
 917			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": clearing "+this.packetCallbacks.size()+" iq callbacks");
 918			final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator = this.packetCallbacks.values().iterator();
 919			while (iterator.hasNext()) {
 920				Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
 921				callbacks.add(entry.second);
 922				iterator.remove();
 923			}
 924		}
 925		for(OnIqPacketReceived callback : callbacks) {
 926			callback.onIqPacketReceived(account,failurePacket);
 927		}
 928		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": done clearing iq callbacks. " + this.packetCallbacks.size() + " left");
 929	}
 930
 931	public void sendDiscoTimeout() {
 932		final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.ERROR); //don't use timeout
 933		final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
 934		synchronized (this.mPendingServiceDiscoveriesIds) {
 935			for(String id : mPendingServiceDiscoveriesIds) {
 936				synchronized (this.packetCallbacks) {
 937					Pair<IqPacket, OnIqPacketReceived> pair = this.packetCallbacks.remove(id);
 938					if (pair != null) {
 939						callbacks.add(pair.second);
 940					}
 941				}
 942			}
 943			this.mPendingServiceDiscoveriesIds.clear();
 944		}
 945		if (callbacks.size() > 0) {
 946			Log.d(Config.LOGTAG,account.getJid().toBareJid()+": sending disco timeout");
 947			resetStreamId(); //we don't want to live with this for ever
 948		}
 949		for(OnIqPacketReceived callback : callbacks) {
 950			callback.onIqPacketReceived(account,failurePacket);
 951		}
 952	}
 953
 954	private void sendStartSession() {
 955		final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
 956		startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
 957		this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() {
 958			@Override
 959			public void onIqPacketReceived(Account account, IqPacket packet) {
 960				if (packet.getType() == IqPacket.TYPE.RESULT) {
 961					sendPostBindInitialization();
 962				} else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
 963					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not init sessions");
 964					disconnect(true);
 965				}
 966			}
 967		});
 968	}
 969
 970	private void sendPostBindInitialization() {
 971		smVersion = 0;
 972		if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
 973			smVersion = 3;
 974		} else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
 975			smVersion = 2;
 976		}
 977		if (smVersion != 0) {
 978			final EnablePacket enable = new EnablePacket(smVersion);
 979			tagWriter.writeStanzaAsync(enable);
 980			stanzasSent = 0;
 981			mStanzaQueue.clear();
 982		}
 983		features.carbonsEnabled = false;
 984		features.blockListRequested = false;
 985		synchronized (this.disco) {
 986			this.disco.clear();
 987		}
 988		mPendingServiceDiscoveries = 0;
 989		lastDiscoStarted = SystemClock.elapsedRealtime();
 990		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery");
 991		mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
 992		sendServiceDiscoveryItems(account.getServer());
 993		sendServiceDiscoveryInfo(account.getServer());
 994		sendServiceDiscoveryInfo(account.getJid().toBareJid());
 995		this.lastSessionStarted = SystemClock.elapsedRealtime();
 996	}
 997
 998	private void sendServiceDiscoveryInfo(final Jid jid) {
 999		mPendingServiceDiscoveries++;
1000		final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1001		iq.setTo(jid);
1002		iq.query("http://jabber.org/protocol/disco#info");
1003		String id = this.sendIqPacket(iq, new OnIqPacketReceived() {
1004
1005			@Override
1006			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1007				if (packet.getType() == IqPacket.TYPE.RESULT) {
1008					boolean advancedStreamFeaturesLoaded = false;
1009					synchronized (XmppConnection.this.disco) {
1010						final List<Element> elements = packet.query().getChildren();
1011						final Info info = new Info();
1012						for (final Element element : elements) {
1013							if (element.getName().equals("identity")) {
1014								String type = element.getAttribute("type");
1015								String category = element.getAttribute("category");
1016								String name = element.getAttribute("name");
1017								if (type != null && category != null) {
1018									info.identities.add(new Pair<>(category, type));
1019									if (type.equals("im") && category.equals("server")) {
1020										if (name != null && jid.equals(account.getServer())) {
1021											switch (name) {
1022												case "Prosody":
1023													mServerIdentity = Identity.PROSODY;
1024													break;
1025												case "ejabberd":
1026													mServerIdentity = Identity.EJABBERD;
1027													break;
1028												case "Slack-XMPP":
1029													mServerIdentity = Identity.SLACK;
1030													break;
1031											}
1032											Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server name: " + name);
1033										}
1034									}
1035								}
1036							} else if (element.getName().equals("feature")) {
1037								info.features.add(element.getAttribute("var"));
1038							}
1039						}
1040						disco.put(jid, info);
1041						advancedStreamFeaturesLoaded = disco.containsKey(account.getServer())
1042								&& disco.containsKey(account.getJid().toBareJid());
1043					}
1044					if (advancedStreamFeaturesLoaded && (jid.equals(account.getServer()) || jid.equals(account.getJid().toBareJid()))) {
1045						enableAdvancedStreamFeatures();
1046					}
1047				} else {
1048					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco info for " + jid.toString());
1049				}
1050				if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1051					mPendingServiceDiscoveries--;
1052					if (mPendingServiceDiscoveries <= 0) {
1053						Log.d(Config.LOGTAG,account.getJid().toBareJid()+": done with service discovery");
1054						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource());
1055						changeStatus(Account.State.ONLINE);
1056						if (bindListener != null) {
1057							bindListener.onBind(account);
1058						}
1059					}
1060				}
1061			}
1062		});
1063		synchronized (this.mPendingServiceDiscoveriesIds) {
1064			this.mPendingServiceDiscoveriesIds.add(id);
1065		}
1066	}
1067
1068	private void enableAdvancedStreamFeatures() {
1069		if (getFeatures().carbons() && !features.carbonsEnabled) {
1070			sendEnableCarbons();
1071		}
1072		if (getFeatures().blocking() && !features.blockListRequested) {
1073			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": Requesting block list");
1074			this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
1075		}
1076		for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
1077			listener.onAdvancedStreamFeaturesAvailable(account);
1078		}
1079	}
1080
1081	private void sendServiceDiscoveryItems(final Jid server) {
1082		final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1083		iq.setTo(server.toDomainJid());
1084		iq.query("http://jabber.org/protocol/disco#items");
1085		this.sendIqPacket(iq, new OnIqPacketReceived() {
1086
1087			@Override
1088			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1089				if (packet.getType() == IqPacket.TYPE.RESULT) {
1090					final List<Element> elements = packet.query().getChildren();
1091					for (final Element element : elements) {
1092						if (element.getName().equals("item")) {
1093							final Jid jid = element.getAttributeAsJid("jid");
1094							if (jid != null && !jid.equals(account.getServer())) {
1095								sendServiceDiscoveryInfo(jid);
1096							}
1097						}
1098					}
1099				} else {
1100					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco items of " + server);
1101				}
1102			}
1103		});
1104	}
1105
1106	private void sendEnableCarbons() {
1107		final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1108		iq.addChild("enable", "urn:xmpp:carbons:2");
1109		this.sendIqPacket(iq, new OnIqPacketReceived() {
1110
1111			@Override
1112			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1113				if (!packet.hasChild("error")) {
1114					Log.d(Config.LOGTAG, account.getJid().toBareJid()
1115							+ ": successfully enabled carbons");
1116					features.carbonsEnabled = true;
1117				} else {
1118					Log.d(Config.LOGTAG, account.getJid().toBareJid()
1119							+ ": error enableing carbons " + packet.toString());
1120				}
1121			}
1122		});
1123	}
1124
1125	private void processStreamError(final Tag currentTag)
1126		throws XmlPullParserException, IOException {
1127		final Element streamError = tagReader.readElement(currentTag);
1128		if (streamError != null && streamError.hasChild("conflict")) {
1129			final String resource = account.getResource().split("\\.")[0];
1130			account.setResource(resource + "." + nextRandomId());
1131			Log.d(Config.LOGTAG,
1132					account.getJid().toBareJid() + ": switching resource due to conflict ("
1133					+ account.getResource() + ")");
1134		} else if (streamError != null) {
1135			Log.d(Config.LOGTAG,account.getJid().toBareJid()+": stream error "+streamError.toString());
1136		}
1137	}
1138
1139	private void sendStartStream() throws IOException {
1140		final Tag stream = Tag.start("stream:stream");
1141		stream.setAttribute("to", account.getServer().toString());
1142		stream.setAttribute("version", "1.0");
1143		stream.setAttribute("xml:lang", "en");
1144		stream.setAttribute("xmlns", "jabber:client");
1145		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
1146		tagWriter.writeTag(stream);
1147	}
1148
1149	private String nextRandomId() {
1150		return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
1151	}
1152
1153	public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
1154		packet.setFrom(account.getJid());
1155		return this.sendUnmodifiedIqPacket(packet, callback);
1156	}
1157
1158	private synchronized String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
1159		if (packet.getId() == null) {
1160			final String id = nextRandomId();
1161			packet.setAttribute("id", id);
1162		}
1163		if (callback != null) {
1164			synchronized (this.packetCallbacks) {
1165				packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
1166			}
1167		}
1168		this.sendPacket(packet);
1169		return packet.getId();
1170	}
1171
1172	public void sendMessagePacket(final MessagePacket packet) {
1173		this.sendPacket(packet);
1174	}
1175
1176	public void sendPresencePacket(final PresencePacket packet) {
1177		this.sendPacket(packet);
1178	}
1179
1180	private synchronized void sendPacket(final AbstractStanza packet) {
1181		if (stanzasSent == Integer.MAX_VALUE) {
1182			resetStreamId();
1183			disconnect(true);
1184			return;
1185		}
1186		tagWriter.writeStanzaAsync(packet);
1187		if (packet instanceof AbstractAcknowledgeableStanza) {
1188			AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
1189			++stanzasSent;
1190			this.mStanzaQueue.put(stanzasSent, stanza);
1191			if (stanza instanceof MessagePacket && stanza.getId() != null && getFeatures().sm()) {
1192				if (Config.EXTENDED_SM_LOGGING) {
1193					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for message stanza #" + stanzasSent);
1194				}
1195				tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
1196			}
1197		}
1198	}
1199
1200	public void sendPing() {
1201		if (!r()) {
1202			final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1203			iq.setFrom(account.getJid());
1204			iq.addChild("ping", "urn:xmpp:ping");
1205			this.sendIqPacket(iq, null);
1206		}
1207		this.lastPingSent = SystemClock.elapsedRealtime();
1208	}
1209
1210	public void setOnMessagePacketReceivedListener(
1211			final OnMessagePacketReceived listener) {
1212		this.messageListener = listener;
1213			}
1214
1215	public void setOnUnregisteredIqPacketReceivedListener(
1216			final OnIqPacketReceived listener) {
1217		this.unregisteredIqListener = listener;
1218			}
1219
1220	public void setOnPresencePacketReceivedListener(
1221			final OnPresencePacketReceived listener) {
1222		this.presenceListener = listener;
1223			}
1224
1225	public void setOnJinglePacketReceivedListener(
1226			final OnJinglePacketReceived listener) {
1227		this.jingleListener = listener;
1228			}
1229
1230	public void setOnStatusChangedListener(final OnStatusChanged listener) {
1231		this.statusListener = listener;
1232	}
1233
1234	public void setOnBindListener(final OnBindListener listener) {
1235		this.bindListener = listener;
1236	}
1237
1238	public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
1239		this.acknowledgedListener = listener;
1240	}
1241
1242	public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) {
1243		if (!this.advancedStreamFeaturesLoadedListeners.contains(listener)) {
1244			this.advancedStreamFeaturesLoadedListeners.add(listener);
1245		}
1246	}
1247
1248	public void disconnect(final boolean force) {
1249		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force));
1250		if (force) {
1251			try {
1252				socket.close();
1253			} catch(Exception e) {
1254				Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": exception during force close ("+e.getMessage()+")");
1255			}
1256			return;
1257		} else {
1258			resetStreamId();
1259			if (tagWriter.isActive()) {
1260				tagWriter.finish();
1261				try {
1262					int i = 0;
1263					boolean warned = false;
1264					while (!tagWriter.finished() && socket.isConnected() && i <= 10) {
1265						if (!warned) {
1266							Log.d(Config.LOGTAG, account.getJid().toBareJid()+": waiting for tag writer to finish");
1267							warned = true;
1268						}
1269						Thread.sleep(200);
1270						i++;
1271					}
1272					if (warned) {
1273						Log.d(Config.LOGTAG,account.getJid().toBareJid()+": tag writer has finished");
1274					}
1275					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closing stream");
1276					tagWriter.writeTag(Tag.end("stream:stream"));
1277				} catch (final IOException e) {
1278					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": io exception during disconnect ("+e.getMessage()+")");
1279				} catch (final InterruptedException e) {
1280					Log.d(Config.LOGTAG, "interrupted");
1281				}
1282			}
1283		}
1284	}
1285
1286	public void resetStreamId() {
1287		this.streamId = null;
1288	}
1289
1290	public List<Jid> findDiscoItemsByFeature(final String feature) {
1291		synchronized (this.disco) {
1292			final List<Jid> items = new ArrayList<>();
1293			for (final Entry<Jid, Info> cursor : this.disco.entrySet()) {
1294				if (cursor.getValue().features.contains(feature)) {
1295					items.add(cursor.getKey());
1296				}
1297			}
1298			return items;
1299		}
1300	}
1301
1302	public Jid findDiscoItemByFeature(final String feature) {
1303		final List<Jid> items = findDiscoItemsByFeature(feature);
1304		if (items.size() >= 1) {
1305			return items.get(0);
1306		}
1307		return null;
1308	}
1309
1310	public boolean r() {
1311		if (getFeatures().sm()) {
1312			this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1313			return true;
1314		} else {
1315			return false;
1316		}
1317	}
1318
1319	public String getMucServer() {
1320		synchronized (this.disco) {
1321			for (final Entry<Jid, Info> cursor : disco.entrySet()) {
1322				final Info value = cursor.getValue();
1323				if (value.features.contains("http://jabber.org/protocol/muc")
1324						&& !value.features.contains("jabber:iq:gateway")
1325						&& !value.identities.contains(new Pair<>("conference", "irc"))) {
1326					return cursor.getKey().toString();
1327				}
1328			}
1329		}
1330		return null;
1331	}
1332
1333	public int getTimeToNextAttempt() {
1334		final int interval = (int) (25 * Math.pow(1.5, attempt));
1335		final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1336		return interval - secondsSinceLast;
1337	}
1338
1339	public int getAttempt() {
1340		return this.attempt;
1341	}
1342
1343	public Features getFeatures() {
1344		return this.features;
1345	}
1346
1347	public long getLastSessionEstablished() {
1348		final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1349		return System.currentTimeMillis() - diff;
1350	}
1351
1352	public long getLastConnect() {
1353		return this.lastConnect;
1354	}
1355
1356	public long getLastPingSent() {
1357		return this.lastPingSent;
1358	}
1359
1360	public long getLastDiscoStarted() {
1361		return this.lastDiscoStarted;
1362	}
1363	public long getLastPacketReceived() {
1364		return this.lastPacketReceived;
1365	}
1366
1367	public void sendActive() {
1368		this.sendPacket(new ActivePacket());
1369	}
1370
1371	public void sendInactive() {
1372		this.sendPacket(new InactivePacket());
1373	}
1374
1375	public void resetAttemptCount() {
1376		this.attempt = 0;
1377		this.lastConnect = 0;
1378	}
1379
1380	public void setInteractive(boolean interactive) {
1381		this.mInteractive = interactive;
1382	}
1383
1384	public Identity getServerIdentity() {
1385		return mServerIdentity;
1386	}
1387
1388	private class Info {
1389		public final ArrayList<String> features = new ArrayList<>();
1390		public final ArrayList<Pair<String,String>> identities = new ArrayList<>();
1391	}
1392
1393	private class UnauthorizedException extends IOException {
1394
1395	}
1396
1397	private class SecurityException extends IOException {
1398
1399	}
1400
1401	private class IncompatibleServerException extends IOException {
1402
1403	}
1404
1405	public enum Identity {
1406		FACEBOOK,
1407		SLACK,
1408		EJABBERD,
1409		PROSODY,
1410		UNKNOWN
1411	}
1412
1413	public class Features {
1414		XmppConnection connection;
1415		private boolean carbonsEnabled = false;
1416		private boolean encryptionEnabled = false;
1417		private boolean blockListRequested = false;
1418
1419		public Features(final XmppConnection connection) {
1420			this.connection = connection;
1421		}
1422
1423		private boolean hasDiscoFeature(final Jid server, final String feature) {
1424			synchronized (XmppConnection.this.disco) {
1425				return connection.disco.containsKey(server) &&
1426						connection.disco.get(server).features.contains(feature);
1427			}
1428		}
1429
1430		public boolean carbons() {
1431			return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1432		}
1433
1434		public boolean blocking() {
1435			return hasDiscoFeature(account.getServer(), Xmlns.BLOCKING);
1436		}
1437
1438		public boolean register() {
1439			return hasDiscoFeature(account.getServer(), Xmlns.REGISTER);
1440		}
1441
1442		public boolean sm() {
1443			return streamId != null
1444					|| (connection.streamFeatures != null && connection.streamFeatures.hasChild("sm"));
1445		}
1446
1447		public boolean csi() {
1448			return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
1449		}
1450
1451		public boolean pep() {
1452			synchronized (XmppConnection.this.disco) {
1453				final Pair<String, String> needle = new Pair<>("pubsub", "pep");
1454				Info info = disco.get(account.getServer());
1455				if (info != null && info.identities.contains(needle)) {
1456					return true;
1457				} else {
1458					info = disco.get(account.getJid().toBareJid());
1459					return info != null && info.identities.contains(needle);
1460				}
1461			}
1462		}
1463
1464		public boolean mam() {
1465			if (hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:mam:0")) {
1466				return true;
1467			} else {
1468				return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0");
1469			}
1470		}
1471
1472		public boolean advancedStreamFeaturesLoaded() {
1473			synchronized (XmppConnection.this.disco) {
1474				return disco.containsKey(account.getServer());
1475			}
1476		}
1477
1478		public boolean rosterVersioning() {
1479			return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
1480		}
1481
1482		public void setBlockListRequested(boolean value) {
1483			this.blockListRequested = value;
1484		}
1485
1486		public boolean httpUpload() {
1487			return !Config.DISABLE_HTTP_UPLOAD && findDiscoItemsByFeature(Xmlns.HTTP_UPLOAD).size() > 0;
1488		}
1489	}
1490
1491	private IqGenerator getIqGenerator() {
1492		return mXmppConnectionService.getIqGenerator();
1493	}
1494}