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