XmppConnection.java

   1package eu.siacs.conversations.xmpp;
   2
   3import java.io.IOException;
   4import java.io.InputStream;
   5import java.io.OutputStream;
   6import java.math.BigInteger;
   7import java.net.Socket;
   8import java.net.UnknownHostException;
   9import java.security.KeyManagementException;
  10import java.security.NoSuchAlgorithmException;
  11import java.security.SecureRandom;
  12import java.util.ArrayList;
  13import java.util.HashMap;
  14import java.util.Hashtable;
  15import java.util.List;
  16import java.util.Map.Entry;
  17
  18import javax.net.ssl.HostnameVerifier;
  19import javax.net.ssl.SSLContext;
  20import javax.net.ssl.SSLSocket;
  21import javax.net.ssl.SSLSocketFactory;
  22
  23import javax.net.ssl.X509TrustManager;
  24
  25import org.xmlpull.v1.XmlPullParserException;
  26
  27import de.duenndns.ssl.MemorizingTrustManager;
  28
  29import android.os.Bundle;
  30import android.os.PowerManager;
  31import android.os.PowerManager.WakeLock;
  32import android.os.SystemClock;
  33import android.util.Log;
  34import android.util.SparseArray;
  35import eu.siacs.conversations.Config;
  36import eu.siacs.conversations.entities.Account;
  37import eu.siacs.conversations.services.XmppConnectionService;
  38import eu.siacs.conversations.utils.CryptoHelper;
  39import eu.siacs.conversations.utils.DNSHelper;
  40import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
  41import eu.siacs.conversations.utils.zlib.ZLibInputStream;
  42import eu.siacs.conversations.xml.Element;
  43import eu.siacs.conversations.xml.Tag;
  44import eu.siacs.conversations.xml.TagWriter;
  45import eu.siacs.conversations.xml.XmlReader;
  46import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  47import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  48import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
  49import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  50import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  51import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  52import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
  53import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
  54import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
  55import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
  56import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
  57import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
  58
  59public class XmppConnection implements Runnable {
  60
  61	protected Account account;
  62
  63	private WakeLock wakeLock;
  64
  65	private SecureRandom mRandom;
  66
  67	private Socket socket;
  68	private XmlReader tagReader;
  69	private TagWriter tagWriter;
  70
  71	private Features features = new Features(this);
  72
  73	private boolean shouldBind = true;
  74	private boolean shouldAuthenticate = true;
  75	private Element streamFeatures;
  76	private HashMap<String, List<String>> disco = new HashMap<String, List<String>>();
  77
  78	private String streamId = null;
  79	private int smVersion = 3;
  80	private SparseArray<String> messageReceipts = new SparseArray<String>();
  81
  82	private boolean usingCompression = false;
  83
  84	private int stanzasReceived = 0;
  85	private int stanzasSent = 0;
  86
  87	private long lastPaketReceived = 0;
  88	private long lastPingSent = 0;
  89	private long lastConnect = 0;
  90	private long lastSessionStarted = 0;
  91
  92	private int attempt = 0;
  93
  94	private static final int PACKET_IQ = 0;
  95	private static final int PACKET_MESSAGE = 1;
  96	private static final int PACKET_PRESENCE = 2;
  97
  98	private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
  99	private OnPresencePacketReceived presenceListener = null;
 100	private OnJinglePacketReceived jingleListener = null;
 101	private OnIqPacketReceived unregisteredIqListener = null;
 102	private OnMessagePacketReceived messageListener = null;
 103	private OnStatusChanged statusListener = null;
 104	private OnBindListener bindListener = null;
 105	private OnMessageAcknowledged acknowledgedListener = null;
 106	private MemorizingTrustManager mMemorizingTrustManager;
 107
 108	public XmppConnection(Account account, XmppConnectionService service) {
 109		this.mRandom = service.getRNG();
 110		this.mMemorizingTrustManager = service.getMemorizingTrustManager();
 111		this.account = account;
 112		this.wakeLock = service.getPowerManager().newWakeLock(
 113				PowerManager.PARTIAL_WAKE_LOCK, account.getJid());
 114		tagWriter = new TagWriter();
 115	}
 116
 117	protected void changeStatus(int nextStatus) {
 118		if (account.getStatus() != nextStatus) {
 119			if ((nextStatus == Account.STATUS_OFFLINE)
 120					&& (account.getStatus() != Account.STATUS_CONNECTING)
 121					&& (account.getStatus() != Account.STATUS_ONLINE)
 122					&& (account.getStatus() != Account.STATUS_DISABLED)) {
 123				return;
 124			}
 125			if (nextStatus == Account.STATUS_ONLINE) {
 126				this.attempt = 0;
 127			}
 128			account.setStatus(nextStatus);
 129			if (statusListener != null) {
 130				statusListener.onStatusChanged(account);
 131			}
 132		}
 133	}
 134
 135	protected void connect() {
 136		Log.d(Config.LOGTAG, account.getJid() + ": connecting");
 137		usingCompression = false;
 138		lastConnect = SystemClock.elapsedRealtime();
 139		lastPingSent = SystemClock.elapsedRealtime();
 140		this.attempt++;
 141		try {
 142			shouldAuthenticate = shouldBind = !account
 143					.isOptionSet(Account.OPTION_REGISTER);
 144			tagReader = new XmlReader(wakeLock);
 145			tagWriter = new TagWriter();
 146			packetCallbacks.clear();
 147			this.changeStatus(Account.STATUS_CONNECTING);
 148			Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
 149			if ("timeout".equals(namePort.getString("error"))) {
 150				Log.d(Config.LOGTAG, account.getJid() + ": dns timeout");
 151				this.changeStatus(Account.STATUS_OFFLINE);
 152				return;
 153			}
 154			String srvRecordServer = namePort.getString("name");
 155			String srvIpServer = namePort.getString("ipv4");
 156			int srvRecordPort = namePort.getInt("port");
 157			if (srvRecordServer != null) {
 158				if (srvIpServer != null) {
 159					Log.d(Config.LOGTAG, account.getJid()
 160							+ ": using values from dns " + srvRecordServer
 161							+ "[" + srvIpServer + "]:" + srvRecordPort);
 162					socket = new Socket(srvIpServer, srvRecordPort);
 163				} else {
 164					boolean socketError = true;
 165					int srvIndex = 0;
 166					while (socketError && namePort.containsKey("name" + srvIndex)){
 167						try {
 168							srvRecordServer = namePort.getString("name" + srvIndex);
 169							srvRecordPort = namePort.getInt("port" + srvIndex);
 170							Log.d(Config.LOGTAG, account.getJid()
 171									+ ": using values from dns " + srvRecordServer
 172									+ ":" + srvRecordPort);
 173							socket = new Socket(srvRecordServer, srvRecordPort);
 174							socketError = false;
 175						} catch (UnknownHostException e) {
 176							srvIndex++;
 177						} catch (IOException e) {
 178							srvIndex++;
 179						}
 180					}
 181				}
 182			} else if (namePort.containsKey("error")
 183					&& "nosrv".equals(namePort.getString("error", null))) {
 184				socket = new Socket(account.getServer(), 5222);
 185			} else {
 186				Log.d(Config.LOGTAG, account.getJid()
 187						+ ": timeout in DNS resolution");
 188				changeStatus(Account.STATUS_OFFLINE);
 189				return;
 190			}
 191			OutputStream out = socket.getOutputStream();
 192			tagWriter.setOutputStream(out);
 193			InputStream in = socket.getInputStream();
 194			tagReader.setInputStream(in);
 195			tagWriter.beginDocument();
 196			sendStartStream();
 197			Tag nextTag;
 198			while ((nextTag = tagReader.readTag()) != null) {
 199				if (nextTag.isStart("stream")) {
 200					processStream(nextTag);
 201					break;
 202				} else {
 203					Log.d(Config.LOGTAG,
 204							"found unexpected tag: " + nextTag.getName());
 205					return;
 206				}
 207			}
 208			if (socket.isConnected()) {
 209				socket.close();
 210			}
 211		} catch (UnknownHostException e) {
 212			this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
 213			if (wakeLock.isHeld()) {
 214				try {
 215					wakeLock.release();
 216				} catch (RuntimeException re) {
 217				}
 218			}
 219			return;
 220		} catch (IOException e) {
 221			this.changeStatus(Account.STATUS_OFFLINE);
 222			if (wakeLock.isHeld()) {
 223				try {
 224					wakeLock.release();
 225				} catch (RuntimeException re) {
 226				}
 227			}
 228			return;
 229		} catch (NoSuchAlgorithmException e) {
 230			this.changeStatus(Account.STATUS_OFFLINE);
 231			Log.d(Config.LOGTAG, "compression exception " + e.getMessage());
 232			if (wakeLock.isHeld()) {
 233				try {
 234					wakeLock.release();
 235				} catch (RuntimeException re) {
 236				}
 237			}
 238			return;
 239		} catch (XmlPullParserException e) {
 240			this.changeStatus(Account.STATUS_OFFLINE);
 241			Log.d(Config.LOGTAG, "xml exception " + e.getMessage());
 242			if (wakeLock.isHeld()) {
 243				try {
 244					wakeLock.release();
 245				} catch (RuntimeException re) {
 246				}
 247			}
 248			return;
 249		}
 250
 251	}
 252
 253	@Override
 254	public void run() {
 255		connect();
 256	}
 257
 258	private void processStream(Tag currentTag) throws XmlPullParserException,
 259			IOException, NoSuchAlgorithmException {
 260		Tag nextTag = tagReader.readTag();
 261		while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
 262			if (nextTag.isStart("error")) {
 263				processStreamError(nextTag);
 264			} else if (nextTag.isStart("features")) {
 265				processStreamFeatures(nextTag);
 266			} else if (nextTag.isStart("proceed")) {
 267				switchOverToTls(nextTag);
 268			} else if (nextTag.isStart("compressed")) {
 269				switchOverToZLib(nextTag);
 270			} else if (nextTag.isStart("success")) {
 271				Log.d(Config.LOGTAG, account.getJid() + ": logged in");
 272				tagReader.readTag();
 273				tagReader.reset();
 274				sendStartStream();
 275				processStream(tagReader.readTag());
 276				break;
 277			} else if (nextTag.isStart("failure")) {
 278				tagReader.readElement(nextTag);
 279				changeStatus(Account.STATUS_UNAUTHORIZED);
 280			} else if (nextTag.isStart("challenge")) {
 281				String challange = tagReader.readElement(nextTag).getContent();
 282				Element response = new Element("response");
 283				response.setAttribute("xmlns",
 284						"urn:ietf:params:xml:ns:xmpp-sasl");
 285				response.setContent(CryptoHelper.saslDigestMd5(account,
 286						challange, mRandom));
 287				tagWriter.writeElement(response);
 288			} else if (nextTag.isStart("enabled")) {
 289				Element enabled = tagReader.readElement(nextTag);
 290				if ("true".equals(enabled.getAttribute("resume"))) {
 291					this.streamId = enabled.getAttribute("id");
 292					Log.d(Config.LOGTAG, account.getJid()
 293							+ ": stream managment(" + smVersion
 294							+ ") enabled (resumable)");
 295				} else {
 296					Log.d(Config.LOGTAG, account.getJid()
 297							+ ": stream managment(" + smVersion + ") enabled");
 298				}
 299				this.lastSessionStarted = SystemClock.elapsedRealtime();
 300				this.stanzasReceived = 0;
 301				RequestPacket r = new RequestPacket(smVersion);
 302				tagWriter.writeStanzaAsync(r);
 303			} else if (nextTag.isStart("resumed")) {
 304				lastPaketReceived = SystemClock.elapsedRealtime();
 305				Element resumed = tagReader.readElement(nextTag);
 306				String h = resumed.getAttribute("h");
 307				try {
 308					int serverCount = Integer.parseInt(h);
 309					if (serverCount != stanzasSent) {
 310						Log.d(Config.LOGTAG, account.getJid()
 311								+ ": session resumed with lost packages");
 312						stanzasSent = serverCount;
 313					} else {
 314						Log.d(Config.LOGTAG, account.getJid()
 315								+ ": session resumed");
 316					}
 317					if (acknowledgedListener != null) {
 318						for (int i = 0; i < messageReceipts.size(); ++i) {
 319							if (serverCount >= messageReceipts.keyAt(i)) {
 320								acknowledgedListener.onMessageAcknowledged(
 321										account, messageReceipts.valueAt(i));
 322							}
 323						}
 324					}
 325					messageReceipts.clear();
 326				} catch (NumberFormatException e) {
 327
 328				}
 329				sendInitialPing();
 330
 331			} else if (nextTag.isStart("r")) {
 332				tagReader.readElement(nextTag);
 333				AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
 334				tagWriter.writeStanzaAsync(ack);
 335			} else if (nextTag.isStart("a")) {
 336				Element ack = tagReader.readElement(nextTag);
 337				lastPaketReceived = SystemClock.elapsedRealtime();
 338				int serverSequence = Integer.parseInt(ack.getAttribute("h"));
 339				String msgId = this.messageReceipts.get(serverSequence);
 340				if (msgId != null) {
 341					if (this.acknowledgedListener != null) {
 342						this.acknowledgedListener.onMessageAcknowledged(
 343								account, msgId);
 344					}
 345					this.messageReceipts.remove(serverSequence);
 346				}
 347			} else if (nextTag.isStart("failed")) {
 348				tagReader.readElement(nextTag);
 349				Log.d(Config.LOGTAG, account.getJid() + ": resumption failed");
 350				streamId = null;
 351				if (account.getStatus() != Account.STATUS_ONLINE) {
 352					sendBindRequest();
 353				}
 354			} else if (nextTag.isStart("iq")) {
 355				processIq(nextTag);
 356			} else if (nextTag.isStart("message")) {
 357				processMessage(nextTag);
 358			} else if (nextTag.isStart("presence")) {
 359				processPresence(nextTag);
 360			}
 361			nextTag = tagReader.readTag();
 362		}
 363		if (account.getStatus() == Account.STATUS_ONLINE) {
 364			account.setStatus(Account.STATUS_OFFLINE);
 365			if (statusListener != null) {
 366				statusListener.onStatusChanged(account);
 367			}
 368		}
 369	}
 370
 371	private void sendInitialPing() {
 372		Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping");
 373		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 374		iq.setFrom(account.getFullJid());
 375		iq.addChild("ping", "urn:xmpp:ping");
 376		this.sendIqPacket(iq, new OnIqPacketReceived() {
 377
 378			@Override
 379			public void onIqPacketReceived(Account account, IqPacket packet) {
 380				Log.d(Config.LOGTAG, account.getJid()
 381						+ ": online with resource " + account.getResource());
 382				changeStatus(Account.STATUS_ONLINE);
 383			}
 384		});
 385	}
 386
 387	private Element processPacket(Tag currentTag, int packetType)
 388			throws XmlPullParserException, IOException {
 389		Element element;
 390		switch (packetType) {
 391		case PACKET_IQ:
 392			element = new IqPacket();
 393			break;
 394		case PACKET_MESSAGE:
 395			element = new MessagePacket();
 396			break;
 397		case PACKET_PRESENCE:
 398			element = new PresencePacket();
 399			break;
 400		default:
 401			return null;
 402		}
 403		element.setAttributes(currentTag.getAttributes());
 404		Tag nextTag = tagReader.readTag();
 405		if (nextTag == null) {
 406			throw new IOException("interrupted mid tag");
 407		}
 408		while (!nextTag.isEnd(element.getName())) {
 409			if (!nextTag.isNo()) {
 410				Element child = tagReader.readElement(nextTag);
 411				String type = currentTag.getAttribute("type");
 412				if (packetType == PACKET_IQ
 413						&& "jingle".equals(child.getName())
 414						&& ("set".equalsIgnoreCase(type) || "get"
 415								.equalsIgnoreCase(type))) {
 416					element = new JinglePacket();
 417					element.setAttributes(currentTag.getAttributes());
 418				}
 419				element.addChild(child);
 420			}
 421			nextTag = tagReader.readTag();
 422			if (nextTag == null) {
 423				throw new IOException("interrupted mid tag");
 424			}
 425		}
 426		++stanzasReceived;
 427		lastPaketReceived = SystemClock.elapsedRealtime();
 428		return element;
 429	}
 430
 431	private void processIq(Tag currentTag) throws XmlPullParserException,
 432			IOException {
 433		IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
 434
 435		if (packet.getId() == null) {
 436			return; // an iq packet without id is definitely invalid
 437		}
 438
 439		if (packet instanceof JinglePacket) {
 440			if (this.jingleListener != null) {
 441				this.jingleListener.onJinglePacketReceived(account,
 442						(JinglePacket) packet);
 443			}
 444		} else {
 445			if (packetCallbacks.containsKey(packet.getId())) {
 446				if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
 447					((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
 448							.onIqPacketReceived(account, packet);
 449				}
 450
 451				packetCallbacks.remove(packet.getId());
 452			} else if ((packet.getType() == IqPacket.TYPE_GET || packet
 453					.getType() == IqPacket.TYPE_SET)
 454					&& this.unregisteredIqListener != null) {
 455				this.unregisteredIqListener.onIqPacketReceived(account, packet);
 456			}
 457		}
 458	}
 459
 460	private void processMessage(Tag currentTag) throws XmlPullParserException,
 461			IOException {
 462		MessagePacket packet = (MessagePacket) processPacket(currentTag,
 463				PACKET_MESSAGE);
 464		String id = packet.getAttribute("id");
 465		if ((id != null) && (packetCallbacks.containsKey(id))) {
 466			if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
 467				((OnMessagePacketReceived) packetCallbacks.get(id))
 468						.onMessagePacketReceived(account, packet);
 469			}
 470			packetCallbacks.remove(id);
 471		} else if (this.messageListener != null) {
 472			this.messageListener.onMessagePacketReceived(account, packet);
 473		}
 474	}
 475
 476	private void processPresence(Tag currentTag) throws XmlPullParserException,
 477			IOException {
 478		PresencePacket packet = (PresencePacket) processPacket(currentTag,
 479				PACKET_PRESENCE);
 480		String id = packet.getAttribute("id");
 481		if ((id != null) && (packetCallbacks.containsKey(id))) {
 482			if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
 483				((OnPresencePacketReceived) packetCallbacks.get(id))
 484						.onPresencePacketReceived(account, packet);
 485			}
 486			packetCallbacks.remove(id);
 487		} else if (this.presenceListener != null) {
 488			this.presenceListener.onPresencePacketReceived(account, packet);
 489		}
 490	}
 491
 492	private void sendCompressionZlib() throws IOException {
 493		Element compress = new Element("compress");
 494		compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
 495		compress.addChild("method").setContent("zlib");
 496		tagWriter.writeElement(compress);
 497	}
 498
 499	private void switchOverToZLib(Tag currentTag)
 500			throws XmlPullParserException, IOException,
 501			NoSuchAlgorithmException {
 502		tagReader.readTag(); // read tag close
 503		tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
 504				.getOutputStream()));
 505		tagReader
 506				.setInputStream(new ZLibInputStream(tagReader.getInputStream()));
 507
 508		sendStartStream();
 509		Log.d(Config.LOGTAG, account.getJid() + ": compression enabled");
 510		usingCompression = true;
 511		processStream(tagReader.readTag());
 512	}
 513
 514	private void sendStartTLS() throws IOException {
 515		Tag startTLS = Tag.empty("starttls");
 516		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
 517		tagWriter.writeTag(startTLS);
 518	}
 519
 520	private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
 521			IOException {
 522		tagReader.readTag();
 523		try {
 524			SSLContext sc = SSLContext.getInstance("TLS");
 525			sc.init(null,
 526					new X509TrustManager[] { this.mMemorizingTrustManager },
 527					mRandom);
 528			SSLSocketFactory factory = sc.getSocketFactory();
 529
 530			HostnameVerifier verifier = this.mMemorizingTrustManager
 531					.wrapHostnameVerifier(new org.apache.http.conn.ssl.StrictHostnameVerifier());
 532			SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
 533					socket.getInetAddress().getHostAddress(), socket.getPort(),
 534					true);
 535
 536			if (verifier != null
 537					&& !verifier.verify(account.getServer(),
 538							sslSocket.getSession())) {
 539				Log.d(Config.LOGTAG, account.getJid()
 540						+ ": host mismatch in TLS connection");
 541				sslSocket.close();
 542				throw new IOException();
 543			}
 544			tagReader.setInputStream(sslSocket.getInputStream());
 545			tagWriter.setOutputStream(sslSocket.getOutputStream());
 546			sendStartStream();
 547			Log.d(Config.LOGTAG, account.getJid()
 548					+ ": TLS connection established");
 549			processStream(tagReader.readTag());
 550			sslSocket.close();
 551		} catch (NoSuchAlgorithmException e1) {
 552			e1.printStackTrace();
 553		} catch (KeyManagementException e) {
 554			e.printStackTrace();
 555		}
 556	}
 557
 558	private void sendSaslAuthPlain() throws IOException {
 559		String saslString = CryptoHelper.saslPlain(account.getUsername(),
 560				account.getPassword());
 561		Element auth = new Element("auth");
 562		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
 563		auth.setAttribute("mechanism", "PLAIN");
 564		auth.setContent(saslString);
 565		tagWriter.writeElement(auth);
 566	}
 567
 568	private void sendSaslAuthDigestMd5() throws IOException {
 569		Element auth = new Element("auth");
 570		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
 571		auth.setAttribute("mechanism", "DIGEST-MD5");
 572		tagWriter.writeElement(auth);
 573	}
 574
 575	private void processStreamFeatures(Tag currentTag)
 576			throws XmlPullParserException, IOException {
 577		this.streamFeatures = tagReader.readElement(currentTag);
 578		if (this.streamFeatures.hasChild("starttls")
 579				&& account.isOptionSet(Account.OPTION_USETLS)) {
 580			sendStartTLS();
 581		} else if (compressionAvailable()) {
 582			sendCompressionZlib();
 583		} else if (this.streamFeatures.hasChild("register")
 584				&& (account.isOptionSet(Account.OPTION_REGISTER))) {
 585			sendRegistryRequest();
 586		} else if (!this.streamFeatures.hasChild("register")
 587				&& (account.isOptionSet(Account.OPTION_REGISTER))) {
 588			changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
 589			disconnect(true);
 590		} else if (this.streamFeatures.hasChild("mechanisms")
 591				&& shouldAuthenticate) {
 592			List<String> mechanisms = extractMechanisms(streamFeatures
 593					.findChild("mechanisms"));
 594			if (mechanisms.contains("PLAIN")) {
 595				sendSaslAuthPlain();
 596			} else if (mechanisms.contains("DIGEST-MD5")) {
 597				sendSaslAuthDigestMd5();
 598			}
 599		} else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
 600				+ smVersion)
 601				&& streamId != null) {
 602			ResumePacket resume = new ResumePacket(this.streamId,
 603					stanzasReceived, smVersion);
 604			this.tagWriter.writeStanzaAsync(resume);
 605		} else if (this.streamFeatures.hasChild("bind") && shouldBind) {
 606			sendBindRequest();
 607		}
 608	}
 609
 610	private boolean compressionAvailable() {
 611		if (!this.streamFeatures.hasChild("compression",
 612				"http://jabber.org/features/compress"))
 613			return false;
 614		if (!ZLibOutputStream.SUPPORTED)
 615			return false;
 616		if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
 617			return false;
 618
 619		Element compression = this.streamFeatures.findChild("compression",
 620				"http://jabber.org/features/compress");
 621		for (Element child : compression.getChildren()) {
 622			if (!"method".equals(child.getName()))
 623				continue;
 624
 625			if ("zlib".equalsIgnoreCase(child.getContent())) {
 626				return true;
 627			}
 628		}
 629		return false;
 630	}
 631
 632	private List<String> extractMechanisms(Element stream) {
 633		ArrayList<String> mechanisms = new ArrayList<String>(stream
 634				.getChildren().size());
 635		for (Element child : stream.getChildren()) {
 636			mechanisms.add(child.getContent());
 637		}
 638		return mechanisms;
 639	}
 640
 641	private void sendRegistryRequest() {
 642		IqPacket register = new IqPacket(IqPacket.TYPE_GET);
 643		register.query("jabber:iq:register");
 644		register.setTo(account.getServer());
 645		sendIqPacket(register, new OnIqPacketReceived() {
 646
 647			@Override
 648			public void onIqPacketReceived(Account account, IqPacket packet) {
 649				Element instructions = packet.query().findChild("instructions");
 650				if (packet.query().hasChild("username")
 651						&& (packet.query().hasChild("password"))) {
 652					IqPacket register = new IqPacket(IqPacket.TYPE_SET);
 653					Element username = new Element("username")
 654							.setContent(account.getUsername());
 655					Element password = new Element("password")
 656							.setContent(account.getPassword());
 657					register.query("jabber:iq:register").addChild(username);
 658					register.query().addChild(password);
 659					sendIqPacket(register, new OnIqPacketReceived() {
 660
 661						@Override
 662						public void onIqPacketReceived(Account account,
 663								IqPacket packet) {
 664							if (packet.getType() == IqPacket.TYPE_RESULT) {
 665								account.setOption(Account.OPTION_REGISTER,
 666										false);
 667								changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
 668							} else if (packet.hasChild("error")
 669									&& (packet.findChild("error")
 670											.hasChild("conflict"))) {
 671								changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
 672							} else {
 673								changeStatus(Account.STATUS_REGISTRATION_FAILED);
 674								Log.d(Config.LOGTAG, packet.toString());
 675							}
 676							disconnect(true);
 677						}
 678					});
 679				} else {
 680					changeStatus(Account.STATUS_REGISTRATION_FAILED);
 681					disconnect(true);
 682					Log.d(Config.LOGTAG, account.getJid()
 683							+ ": could not register. instructions are"
 684							+ instructions.getContent());
 685				}
 686			}
 687		});
 688	}
 689
 690	private void sendBindRequest() throws IOException {
 691		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
 692		iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
 693				.addChild("resource").setContent(account.getResource());
 694		this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
 695			@Override
 696			public void onIqPacketReceived(Account account, IqPacket packet) {
 697				Element bind = packet.findChild("bind");
 698				if (bind != null) {
 699					Element jid = bind.findChild("jid");
 700					if (jid != null && jid.getContent() != null) {
 701						account.setResource(jid.getContent().split("/", 2)[1]);
 702						if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
 703							smVersion = 3;
 704							EnablePacket enable = new EnablePacket(smVersion);
 705							tagWriter.writeStanzaAsync(enable);
 706							stanzasSent = 0;
 707							messageReceipts.clear();
 708						} else if (streamFeatures.hasChild("sm",
 709								"urn:xmpp:sm:2")) {
 710							smVersion = 2;
 711							EnablePacket enable = new EnablePacket(smVersion);
 712							tagWriter.writeStanzaAsync(enable);
 713							stanzasSent = 0;
 714							messageReceipts.clear();
 715						}
 716						sendServiceDiscoveryInfo(account.getServer());
 717						sendServiceDiscoveryItems(account.getServer());
 718						if (bindListener != null) {
 719							bindListener.onBind(account);
 720						}
 721						sendInitialPing();
 722					} else {
 723						disconnect(true);
 724					}
 725				} else {
 726					disconnect(true);
 727				}
 728			}
 729		});
 730		if (this.streamFeatures.hasChild("session")) {
 731			Log.d(Config.LOGTAG, account.getJid()
 732					+ ": sending deprecated session");
 733			IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
 734			startSession.addChild("session",
 735					"urn:ietf:params:xml:ns:xmpp-session");
 736			this.sendUnboundIqPacket(startSession, null);
 737		}
 738	}
 739
 740	private void sendServiceDiscoveryInfo(final String server) {
 741		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 742		iq.setTo(server);
 743		iq.query("http://jabber.org/protocol/disco#info");
 744		this.sendIqPacket(iq, new OnIqPacketReceived() {
 745
 746			@Override
 747			public void onIqPacketReceived(Account account, IqPacket packet) {
 748				List<Element> elements = packet.query().getChildren();
 749				List<String> features = new ArrayList<String>();
 750				for (int i = 0; i < elements.size(); ++i) {
 751					if (elements.get(i).getName().equals("feature")) {
 752						features.add(elements.get(i).getAttribute("var"));
 753					}
 754				}
 755				disco.put(server, features);
 756
 757				if (account.getServer().equals(server)) {
 758					enableAdvancedStreamFeatures();
 759				}
 760			}
 761		});
 762	}
 763
 764	private void enableAdvancedStreamFeatures() {
 765		if (getFeatures().carbons()) {
 766			sendEnableCarbons();
 767		}
 768	}
 769
 770	private void sendServiceDiscoveryItems(final String server) {
 771		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 772		iq.setTo(server);
 773		iq.query("http://jabber.org/protocol/disco#items");
 774		this.sendIqPacket(iq, new OnIqPacketReceived() {
 775
 776			@Override
 777			public void onIqPacketReceived(Account account, IqPacket packet) {
 778				List<Element> elements = packet.query().getChildren();
 779				for (int i = 0; i < elements.size(); ++i) {
 780					if (elements.get(i).getName().equals("item")) {
 781						String jid = elements.get(i).getAttribute("jid");
 782						sendServiceDiscoveryInfo(jid);
 783					}
 784				}
 785			}
 786		});
 787	}
 788
 789	private void sendEnableCarbons() {
 790		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
 791		iq.addChild("enable", "urn:xmpp:carbons:2");
 792		this.sendIqPacket(iq, new OnIqPacketReceived() {
 793
 794			@Override
 795			public void onIqPacketReceived(Account account, IqPacket packet) {
 796				if (!packet.hasChild("error")) {
 797					Log.d(Config.LOGTAG, account.getJid()
 798							+ ": successfully enabled carbons");
 799				} else {
 800					Log.d(Config.LOGTAG, account.getJid()
 801							+ ": error enableing carbons " + packet.toString());
 802				}
 803			}
 804		});
 805	}
 806
 807	private void processStreamError(Tag currentTag)
 808			throws XmlPullParserException, IOException {
 809		Element streamError = tagReader.readElement(currentTag);
 810		if (streamError != null && streamError.hasChild("conflict")) {
 811			String resource = account.getResource().split("\\.")[0];
 812			account.setResource(resource + "." + nextRandomId());
 813			Log.d(Config.LOGTAG,
 814					account.getJid() + ": switching resource due to conflict ("
 815							+ account.getResource() + ")");
 816		}
 817	}
 818
 819	private void sendStartStream() throws IOException {
 820		Tag stream = Tag.start("stream:stream");
 821		stream.setAttribute("from", account.getJid());
 822		stream.setAttribute("to", account.getServer());
 823		stream.setAttribute("version", "1.0");
 824		stream.setAttribute("xml:lang", "en");
 825		stream.setAttribute("xmlns", "jabber:client");
 826		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
 827		tagWriter.writeTag(stream);
 828	}
 829
 830	private String nextRandomId() {
 831		return new BigInteger(50, mRandom).toString(32);
 832	}
 833
 834	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
 835		if (packet.getId() == null) {
 836			String id = nextRandomId();
 837			packet.setAttribute("id", id);
 838		}
 839		packet.setFrom(account.getFullJid());
 840		this.sendPacket(packet, callback);
 841	}
 842
 843	public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
 844		if (packet.getId() == null) {
 845			String id = nextRandomId();
 846			packet.setAttribute("id", id);
 847		}
 848		this.sendPacket(packet, callback);
 849	}
 850
 851	public void sendMessagePacket(MessagePacket packet) {
 852		this.sendPacket(packet, null);
 853	}
 854
 855	public void sendPresencePacket(PresencePacket packet) {
 856		this.sendPacket(packet, null);
 857	}
 858
 859	private synchronized void sendPacket(final AbstractStanza packet,
 860			PacketReceived callback) {
 861		if (packet.getName().equals("iq") || packet.getName().equals("message")
 862				|| packet.getName().equals("presence")) {
 863			++stanzasSent;
 864		}
 865		tagWriter.writeStanzaAsync(packet);
 866		if (packet instanceof MessagePacket && packet.getId() != null
 867				&& this.streamId != null) {
 868			Log.d(Config.LOGTAG, "request delivery report for stanza "
 869					+ stanzasSent);
 870			this.messageReceipts.put(stanzasSent, packet.getId());
 871			tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
 872		}
 873		if (callback != null) {
 874			if (packet.getId() == null) {
 875				packet.setId(nextRandomId());
 876			}
 877			packetCallbacks.put(packet.getId(), callback);
 878		}
 879	}
 880
 881	public void sendPing() {
 882		if (streamFeatures.hasChild("sm")) {
 883			tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
 884		} else {
 885			IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 886			iq.setFrom(account.getFullJid());
 887			iq.addChild("ping", "urn:xmpp:ping");
 888			this.sendIqPacket(iq, null);
 889		}
 890		this.lastPingSent = SystemClock.elapsedRealtime();
 891	}
 892
 893	public void setOnMessagePacketReceivedListener(
 894			OnMessagePacketReceived listener) {
 895		this.messageListener = listener;
 896	}
 897
 898	public void setOnUnregisteredIqPacketReceivedListener(
 899			OnIqPacketReceived listener) {
 900		this.unregisteredIqListener = listener;
 901	}
 902
 903	public void setOnPresencePacketReceivedListener(
 904			OnPresencePacketReceived listener) {
 905		this.presenceListener = listener;
 906	}
 907
 908	public void setOnJinglePacketReceivedListener(
 909			OnJinglePacketReceived listener) {
 910		this.jingleListener = listener;
 911	}
 912
 913	public void setOnStatusChangedListener(OnStatusChanged listener) {
 914		this.statusListener = listener;
 915	}
 916
 917	public void setOnBindListener(OnBindListener listener) {
 918		this.bindListener = listener;
 919	}
 920
 921	public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
 922		this.acknowledgedListener = listener;
 923	}
 924
 925	public void disconnect(boolean force) {
 926		Log.d(Config.LOGTAG, account.getJid() + ": disconnecting");
 927		try {
 928			if (force) {
 929				socket.close();
 930				return;
 931			}
 932			new Thread(new Runnable() {
 933
 934				@Override
 935				public void run() {
 936					if (tagWriter.isActive()) {
 937						tagWriter.finish();
 938						try {
 939							while (!tagWriter.finished()) {
 940								Log.d(Config.LOGTAG, "not yet finished");
 941								Thread.sleep(100);
 942							}
 943							tagWriter.writeTag(Tag.end("stream:stream"));
 944							socket.close();
 945						} catch (IOException e) {
 946							Log.d(Config.LOGTAG,
 947									"io exception during disconnect");
 948						} catch (InterruptedException e) {
 949							Log.d(Config.LOGTAG, "interrupted");
 950						}
 951					}
 952				}
 953			}).start();
 954		} catch (IOException e) {
 955			Log.d(Config.LOGTAG, "io exception during disconnect");
 956		}
 957	}
 958
 959	public List<String> findDiscoItemsByFeature(String feature) {
 960		List<String> items = new ArrayList<String>();
 961		for (Entry<String, List<String>> cursor : disco.entrySet()) {
 962			if (cursor.getValue().contains(feature)) {
 963				items.add(cursor.getKey());
 964			}
 965		}
 966		return items;
 967	}
 968
 969	public String findDiscoItemByFeature(String feature) {
 970		List<String> items = findDiscoItemsByFeature(feature);
 971		if (items.size() >= 1) {
 972			return items.get(0);
 973		}
 974		return null;
 975	}
 976
 977	public void r() {
 978		this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
 979	}
 980
 981	public String getMucServer() {
 982		return findDiscoItemByFeature("http://jabber.org/protocol/muc");
 983	}
 984
 985	public int getTimeToNextAttempt() {
 986		int interval = (int) (25 * Math.pow(1.5, attempt));
 987		int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
 988		return interval - secondsSinceLast;
 989	}
 990
 991	public int getAttempt() {
 992		return this.attempt;
 993	}
 994
 995	public Features getFeatures() {
 996		return this.features;
 997	}
 998
 999	public class Features {
1000		XmppConnection connection;
1001
1002		public Features(XmppConnection connection) {
1003			this.connection = connection;
1004		}
1005
1006		private boolean hasDiscoFeature(String server, String feature) {
1007			if (!connection.disco.containsKey(server)) {
1008				return false;
1009			}
1010			return connection.disco.get(server).contains(feature);
1011		}
1012
1013		public boolean carbons() {
1014			return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1015		}
1016
1017		public boolean sm() {
1018			return streamId != null;
1019		}
1020
1021		public boolean csi() {
1022			if (connection.streamFeatures == null) {
1023				return false;
1024			} else {
1025				return connection.streamFeatures.hasChild("csi",
1026						"urn:xmpp:csi:0");
1027			}
1028		}
1029
1030		public boolean pubsub() {
1031			return hasDiscoFeature(account.getServer(),
1032					"http://jabber.org/protocol/pubsub#publish");
1033		}
1034
1035		public boolean rosterVersioning() {
1036			if (connection.streamFeatures == null) {
1037				return false;
1038			} else {
1039				return connection.streamFeatures.hasChild("ver");
1040			}
1041		}
1042
1043		public boolean streamhost() {
1044			return connection
1045					.findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
1046		}
1047
1048		public boolean compression() {
1049			return connection.usingCompression;
1050		}
1051	}
1052
1053	public long getLastSessionEstablished() {
1054		long diff;
1055		if (this.lastSessionStarted == 0) {
1056			diff = SystemClock.elapsedRealtime() - this.lastConnect;
1057		} else {
1058			diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1059		}
1060		return System.currentTimeMillis() - diff;
1061	}
1062
1063	public long getLastConnect() {
1064		return this.lastConnect;
1065	}
1066
1067	public long getLastPingSent() {
1068		return this.lastPingSent;
1069	}
1070
1071	public long getLastPacketReceived() {
1072		return this.lastPaketReceived;
1073	}
1074
1075	public void sendActive() {
1076		this.sendPacket(new ActivePacket(), null);
1077	}
1078
1079	public void sendInactive() {
1080		this.sendPacket(new InactivePacket(), null);
1081	}
1082}