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.KeyStore;
 11import java.security.KeyStoreException;
 12import java.security.MessageDigest;
 13import java.security.NoSuchAlgorithmException;
 14import java.security.SecureRandom;
 15import java.security.cert.CertPathValidatorException;
 16import java.security.cert.CertificateException;
 17import java.security.cert.X509Certificate;
 18import java.util.ArrayList;
 19import java.util.HashMap;
 20import java.util.Hashtable;
 21import java.util.List;
 22import java.util.Map.Entry;
 23
 24import javax.net.ssl.HostnameVerifier;
 25import javax.net.ssl.SSLContext;
 26import javax.net.ssl.SSLSocket;
 27import javax.net.ssl.SSLSocketFactory;
 28import javax.net.ssl.TrustManager;
 29import javax.net.ssl.TrustManagerFactory;
 30import javax.net.ssl.X509TrustManager;
 31
 32import org.bouncycastle.pqc.math.linearalgebra.GoppaCode.MaMaPe;
 33import org.xmlpull.v1.XmlPullParserException;
 34
 35import de.duenndns.ssl.MemorizingTrustManager;
 36
 37import android.content.Context;
 38import android.os.Bundle;
 39import android.os.PowerManager;
 40import android.os.PowerManager.WakeLock;
 41import android.os.SystemClock;
 42import android.util.Log;
 43import eu.siacs.conversations.entities.Account;
 44import eu.siacs.conversations.services.XmppConnectionService;
 45import eu.siacs.conversations.utils.CryptoHelper;
 46import eu.siacs.conversations.utils.DNSHelper;
 47import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
 48import eu.siacs.conversations.utils.zlib.ZLibInputStream;
 49import eu.siacs.conversations.xml.Element;
 50import eu.siacs.conversations.xml.Tag;
 51import eu.siacs.conversations.xml.TagWriter;
 52import eu.siacs.conversations.xml.XmlReader;
 53import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
 54import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 55import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
 56import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 57import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 58import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 59import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
 60import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
 61import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
 62import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
 63
 64public class XmppConnection implements Runnable {
 65
 66	protected Account account;
 67	private static final String LOGTAG = "xmppService";
 68
 69	private WakeLock wakeLock;
 70
 71	private SecureRandom mRandom;
 72
 73	private Socket socket;
 74	private XmlReader tagReader;
 75	private TagWriter tagWriter;
 76
 77	private boolean shouldBind = true;
 78	private boolean shouldAuthenticate = true;
 79	private Element streamFeatures;
 80	private HashMap<String, List<String>> disco = new HashMap<String, List<String>>();
 81
 82	private String streamId = null;
 83	private int smVersion = 3;
 84
 85	private int stanzasReceived = 0;
 86	private int stanzasSent = 0;
 87
 88	public long lastPaketReceived = 0;
 89	public long lastPingSent = 0;
 90	public long lastConnect = 0;
 91	public long lastSessionStarted = 0;
 92
 93	private int attempt = 0;
 94
 95	private static final int PACKET_IQ = 0;
 96	private static final int PACKET_MESSAGE = 1;
 97	private static final int PACKET_PRESENCE = 2;
 98
 99	private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
100	private OnPresencePacketReceived presenceListener = null;
101	private OnJinglePacketReceived jingleListener = null;
102	private OnIqPacketReceived unregisteredIqListener = null;
103	private OnMessagePacketReceived messageListener = null;
104	private OnStatusChanged statusListener = null;
105	private OnBindListener bindListener = 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		lastConnect = SystemClock.elapsedRealtime();
138		this.attempt++;
139		try {
140			shouldAuthenticate = shouldBind = !account
141					.isOptionSet(Account.OPTION_REGISTER);
142			tagReader = new XmlReader(wakeLock);
143			tagWriter = new TagWriter();
144			packetCallbacks.clear();
145			this.changeStatus(Account.STATUS_CONNECTING);
146			Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
147			if ("timeout".equals(namePort.getString("error"))) {
148				Log.d(LOGTAG, account.getJid() + ": dns timeout");
149				this.changeStatus(Account.STATUS_OFFLINE);
150				return;
151			}
152			String srvRecordServer = namePort.getString("name");
153			String srvIpServer = namePort.getString("ipv4");
154			int srvRecordPort = namePort.getInt("port");
155			if (srvRecordServer != null) {
156				if (srvIpServer != null) {
157					Log.d(LOGTAG, account.getJid() + ": using values from dns "
158							+ srvRecordServer + "[" + srvIpServer + "]:"
159							+ srvRecordPort);
160					socket = new Socket(srvIpServer, srvRecordPort);
161				} else {
162					Log.d(LOGTAG, account.getJid() + ": using values from dns "
163							+ srvRecordServer + ":" + srvRecordPort);
164					socket = new Socket(srvRecordServer, srvRecordPort);
165				}
166			} else {
167				socket = new Socket(account.getServer(), 5222);
168			}
169			OutputStream out = socket.getOutputStream();
170			tagWriter.setOutputStream(out);
171			InputStream in = socket.getInputStream();
172			tagReader.setInputStream(in);
173			tagWriter.beginDocument();
174			sendStartStream();
175			Tag nextTag;
176			while ((nextTag = tagReader.readTag()) != null) {
177				if (nextTag.isStart("stream")) {
178					processStream(nextTag);
179					break;
180				} else {
181					Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
182					return;
183				}
184			}
185			if (socket.isConnected()) {
186				socket.close();
187			}
188		} catch (UnknownHostException e) {
189			this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
190			if (wakeLock.isHeld()) {
191				try { wakeLock.release();} catch (RuntimeException re) {}
192			}
193			return;
194		} catch (IOException e) {
195			if (account.getStatus() != Account.STATUS_TLS_ERROR) {
196				this.changeStatus(Account.STATUS_OFFLINE);
197			}
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				if ((streamFeatures.getChildren().size() == 1)
234						&& (streamFeatures.hasChild("starttls"))
235						&& (!account.isOptionSet(Account.OPTION_USETLS))) {
236					changeStatus(Account.STATUS_SERVER_REQUIRES_TLS);
237				}
238			} else if (nextTag.isStart("proceed")) {
239				switchOverToTls(nextTag);
240			} else if (nextTag.isStart("compressed")) {
241				switchOverToZLib(nextTag);
242			} else if (nextTag.isStart("success")) {
243				Log.d(LOGTAG, account.getJid() + ": logged in");
244				tagReader.readTag();
245				tagReader.reset();
246				sendStartStream();
247				processStream(tagReader.readTag());
248				break;
249			} else if (nextTag.isStart("failure")) {
250				tagReader.readElement(nextTag);
251				changeStatus(Account.STATUS_UNAUTHORIZED);
252			} else if (nextTag.isStart("challenge")) {
253				String challange = tagReader.readElement(nextTag).getContent();
254				Element response = new Element("response");
255				response.setAttribute("xmlns",
256						"urn:ietf:params:xml:ns:xmpp-sasl");
257				response.setContent(CryptoHelper.saslDigestMd5(account,
258						challange,mRandom));
259				tagWriter.writeElement(response);
260			} else if (nextTag.isStart("enabled")) {
261				this.stanzasSent = 0;
262				Element enabled = tagReader.readElement(nextTag);
263				if ("true".equals(enabled.getAttribute("resume"))) {
264					this.streamId = enabled.getAttribute("id");
265					Log.d(LOGTAG, account.getJid() + ": stream managment("
266							+ smVersion + ") enabled (resumable)");
267				} else {
268					Log.d(LOGTAG, account.getJid() + ": stream managment("
269							+ smVersion + ") enabled");
270				}
271				this.lastSessionStarted = SystemClock.elapsedRealtime();
272				this.stanzasReceived = 0;
273				RequestPacket r = new RequestPacket(smVersion);
274				tagWriter.writeStanzaAsync(r);
275			} else if (nextTag.isStart("resumed")) {
276				lastPaketReceived = SystemClock.elapsedRealtime();
277				Log.d(LOGTAG, account.getJid() + ": session resumed");
278				tagReader.readElement(nextTag);
279				sendPing();
280				changeStatus(Account.STATUS_ONLINE);
281			} else if (nextTag.isStart("r")) {
282				tagReader.readElement(nextTag);
283				AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
284				tagWriter.writeStanzaAsync(ack);
285			} else if (nextTag.isStart("a")) {
286				Element ack = tagReader.readElement(nextTag);
287				lastPaketReceived = SystemClock.elapsedRealtime();
288				int serverSequence = Integer.parseInt(ack.getAttribute("h"));
289				if (serverSequence > this.stanzasSent) {
290					this.stanzasSent = serverSequence;
291				}
292			} else if (nextTag.isStart("failed")) {
293				tagReader.readElement(nextTag);
294				Log.d(LOGTAG, account.getJid() + ": resumption failed");
295				streamId = null;
296				if (account.getStatus() != Account.STATUS_ONLINE) {
297					sendBindRequest();
298				}
299			} else if (nextTag.isStart("iq")) {
300				processIq(nextTag);
301			} else if (nextTag.isStart("message")) {
302				processMessage(nextTag);
303			} else if (nextTag.isStart("presence")) {
304				processPresence(nextTag);
305			}
306			nextTag = tagReader.readTag();
307		}
308		if (account.getStatus() == Account.STATUS_ONLINE) {
309			account.setStatus(Account.STATUS_OFFLINE);
310			if (statusListener != null) {
311				statusListener.onStatusChanged(account);
312			}
313		}
314	}
315
316	private Element processPacket(Tag currentTag, int packetType)
317			throws XmlPullParserException, IOException {
318		Element element;
319		switch (packetType) {
320		case PACKET_IQ:
321			element = new IqPacket();
322			break;
323		case PACKET_MESSAGE:
324			element = new MessagePacket();
325			break;
326		case PACKET_PRESENCE:
327			element = new PresencePacket();
328			break;
329		default:
330			return null;
331		}
332		element.setAttributes(currentTag.getAttributes());
333		Tag nextTag = tagReader.readTag();
334		if (nextTag==null) {
335			throw new IOException("interrupted mid tag");
336		}
337		while (!nextTag.isEnd(element.getName())) {
338			if (!nextTag.isNo()) {
339				Element child = tagReader.readElement(nextTag);
340				if ((packetType == PACKET_IQ)
341						&& ("jingle".equals(child.getName()))) {
342					element = new JinglePacket();
343					element.setAttributes(currentTag.getAttributes());
344				}
345				element.addChild(child);
346			}
347			nextTag = tagReader.readTag();
348			if (nextTag==null) {
349				throw new IOException("interrupted mid tag");
350			}
351		}
352		++stanzasReceived;
353		lastPaketReceived = SystemClock.elapsedRealtime();
354		return element;
355	}
356
357	private void processIq(Tag currentTag) throws XmlPullParserException,
358			IOException {
359		IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
360
361		if (packet.getId() == null) {
362			return; // an iq packet without id is definitely invalid
363		}
364
365		if (packet instanceof JinglePacket) {
366			if (this.jingleListener != null) {
367				this.jingleListener.onJinglePacketReceived(account,
368						(JinglePacket) packet);
369			}
370		} else {
371			if (packetCallbacks.containsKey(packet.getId())) {
372				if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
373					((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
374							.onIqPacketReceived(account, packet);
375				}
376
377				packetCallbacks.remove(packet.getId());
378			} else if (this.unregisteredIqListener != null) {
379				this.unregisteredIqListener.onIqPacketReceived(account, packet);
380			}
381		}
382	}
383
384	private void processMessage(Tag currentTag) throws XmlPullParserException,
385			IOException {
386		MessagePacket packet = (MessagePacket) processPacket(currentTag,
387				PACKET_MESSAGE);
388		String id = packet.getAttribute("id");
389		if ((id != null) && (packetCallbacks.containsKey(id))) {
390			if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
391				((OnMessagePacketReceived) packetCallbacks.get(id))
392						.onMessagePacketReceived(account, packet);
393			}
394			packetCallbacks.remove(id);
395		} else if (this.messageListener != null) {
396			this.messageListener.onMessagePacketReceived(account, packet);
397		}
398	}
399
400	private void processPresence(Tag currentTag) throws XmlPullParserException,
401			IOException {
402		PresencePacket packet = (PresencePacket) processPacket(currentTag,
403				PACKET_PRESENCE);
404		String id = packet.getAttribute("id");
405		if ((id != null) && (packetCallbacks.containsKey(id))) {
406			if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
407				((OnPresencePacketReceived) packetCallbacks.get(id))
408						.onPresencePacketReceived(account, packet);
409			}
410			packetCallbacks.remove(id);
411		} else if (this.presenceListener != null) {
412			this.presenceListener.onPresencePacketReceived(account, packet);
413		}
414	}
415
416	private void sendCompressionZlib() throws IOException {
417		Element compress = new Element("compress");
418		compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
419		compress.addChild("method").setContent("zlib");
420		tagWriter.writeElement(compress);
421	}
422
423	private void switchOverToZLib(Tag currentTag)
424			throws XmlPullParserException, IOException,
425			NoSuchAlgorithmException {
426		tagReader.readTag(); // read tag close
427
428		tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
429				.getOutputStream()));
430		tagReader
431				.setInputStream(new ZLibInputStream(tagReader.getInputStream()));
432
433		sendStartStream();
434		Log.d(LOGTAG, account.getJid() + ": compression enabled");
435		processStream(tagReader.readTag());
436	}
437
438	private void sendStartTLS() throws IOException {
439		Tag startTLS = Tag.empty("starttls");
440		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
441		tagWriter.writeTag(startTLS);
442	}
443
444	private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
445			IOException {
446		tagReader.readTag();
447		try {
448			SSLContext sc = SSLContext.getInstance("TLS");
449			sc.init(null, new X509TrustManager[] { this.mMemorizingTrustManager }, mRandom);
450			SSLSocketFactory factory = sc.getSocketFactory();
451			
452			HostnameVerifier verifier = this.mMemorizingTrustManager.wrapHostnameVerifier(new org.apache.http.conn.ssl.StrictHostnameVerifier());
453			SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
454					socket.getInetAddress().getHostAddress(), socket.getPort(),
455					true);
456			
457			if (verifier != null && !verifier.verify(account.getServer(), sslSocket.getSession())) {
458				Log.d(LOGTAG, account.getJid() + ": host mismatch in TLS connection");
459				sslSocket.close();
460				throw new IOException();
461			}
462			tagReader.setInputStream(sslSocket.getInputStream());
463			tagWriter.setOutputStream(sslSocket.getOutputStream());
464			sendStartStream();
465			Log.d(LOGTAG, account.getJid() + ": TLS connection established");
466			processStream(tagReader.readTag());
467			sslSocket.close();
468		} catch (NoSuchAlgorithmException e1) {
469			e1.printStackTrace();
470		} catch (KeyManagementException e) {
471			e.printStackTrace();
472		}
473	}
474
475	private void sendSaslAuthPlain() throws IOException {
476		String saslString = CryptoHelper.saslPlain(account.getUsername(),
477				account.getPassword());
478		Element auth = new Element("auth");
479		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
480		auth.setAttribute("mechanism", "PLAIN");
481		auth.setContent(saslString);
482		tagWriter.writeElement(auth);
483	}
484
485	private void sendSaslAuthDigestMd5() throws IOException {
486		Element auth = new Element("auth");
487		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
488		auth.setAttribute("mechanism", "DIGEST-MD5");
489		tagWriter.writeElement(auth);
490	}
491
492	private void processStreamFeatures(Tag currentTag)
493			throws XmlPullParserException, IOException {
494		this.streamFeatures = tagReader.readElement(currentTag);
495		if (this.streamFeatures.hasChild("starttls")
496				&& account.isOptionSet(Account.OPTION_USETLS)) {
497			sendStartTLS();
498		} else if (compressionAvailable()) {
499			sendCompressionZlib();
500		} else if (this.streamFeatures.hasChild("register")
501				&& (account.isOptionSet(Account.OPTION_REGISTER))) {
502			sendRegistryRequest();
503		} else if (!this.streamFeatures.hasChild("register")
504				&& (account.isOptionSet(Account.OPTION_REGISTER))) {
505			changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
506			disconnect(true);
507		} else if (this.streamFeatures.hasChild("mechanisms")
508				&& shouldAuthenticate) {
509			List<String> mechanisms = extractMechanisms(streamFeatures
510					.findChild("mechanisms"));
511			if (mechanisms.contains("PLAIN")) {
512				sendSaslAuthPlain();
513			} else if (mechanisms.contains("DIGEST-MD5")) {
514				sendSaslAuthDigestMd5();
515			}
516		} else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
517				+ smVersion)
518				&& streamId != null) {
519			ResumePacket resume = new ResumePacket(this.streamId,
520					stanzasReceived, smVersion);
521			this.tagWriter.writeStanzaAsync(resume);
522		} else if (this.streamFeatures.hasChild("bind") && shouldBind) {
523			sendBindRequest();
524		}
525	}
526
527	private boolean compressionAvailable() {
528		if (!this.streamFeatures.hasChild("compression",
529				"http://jabber.org/features/compress"))
530			return false;
531		if (!ZLibOutputStream.SUPPORTED)
532			return false;
533		if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
534			return false;
535
536		Element compression = this.streamFeatures.findChild("compression",
537				"http://jabber.org/features/compress");
538		for (Element child : compression.getChildren()) {
539			if (!"method".equals(child.getName()))
540				continue;
541
542			if ("zlib".equalsIgnoreCase(child.getContent())) {
543				return true;
544			}
545		}
546		return false;
547	}
548
549	private List<String> extractMechanisms(Element stream) {
550		ArrayList<String> mechanisms = new ArrayList<String>(stream
551				.getChildren().size());
552		for (Element child : stream.getChildren()) {
553			mechanisms.add(child.getContent());
554		}
555		return mechanisms;
556	}
557
558	private void sendRegistryRequest() {
559		IqPacket register = new IqPacket(IqPacket.TYPE_GET);
560		register.query("jabber:iq:register");
561		register.setTo(account.getServer());
562		sendIqPacket(register, new OnIqPacketReceived() {
563
564			@Override
565			public void onIqPacketReceived(Account account, IqPacket packet) {
566				Element instructions = packet.query().findChild("instructions");
567				if (packet.query().hasChild("username")
568						&& (packet.query().hasChild("password"))) {
569					IqPacket register = new IqPacket(IqPacket.TYPE_SET);
570					Element username = new Element("username")
571							.setContent(account.getUsername());
572					Element password = new Element("password")
573							.setContent(account.getPassword());
574					register.query("jabber:iq:register").addChild(username);
575					register.query().addChild(password);
576					sendIqPacket(register, new OnIqPacketReceived() {
577
578						@Override
579						public void onIqPacketReceived(Account account,
580								IqPacket packet) {
581							if (packet.getType() == IqPacket.TYPE_RESULT) {
582								account.setOption(Account.OPTION_REGISTER,
583										false);
584								changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
585							} else if (packet.hasChild("error")
586									&& (packet.findChild("error")
587											.hasChild("conflict"))) {
588								changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
589							} else {
590								changeStatus(Account.STATUS_REGISTRATION_FAILED);
591								Log.d(LOGTAG, packet.toString());
592							}
593							disconnect(true);
594						}
595					});
596				} else {
597					changeStatus(Account.STATUS_REGISTRATION_FAILED);
598					disconnect(true);
599					Log.d(LOGTAG, account.getJid()
600							+ ": could not register. instructions are"
601							+ instructions.getContent());
602				}
603			}
604		});
605	}
606
607	private void sendBindRequest() throws IOException {
608		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
609		iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
610				.addChild("resource").setContent(account.getResource());
611		this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
612			@Override
613			public void onIqPacketReceived(Account account, IqPacket packet) {
614				String resource = packet.findChild("bind").findChild("jid")
615						.getContent().split("/")[1];
616				account.setResource(resource);
617				if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
618					smVersion = 3;
619					EnablePacket enable = new EnablePacket(smVersion);
620					tagWriter.writeStanzaAsync(enable);
621				} else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
622					smVersion = 2;
623					EnablePacket enable = new EnablePacket(smVersion);
624					tagWriter.writeStanzaAsync(enable);
625				}
626				sendServiceDiscoveryInfo(account.getServer());
627				sendServiceDiscoveryItems(account.getServer());
628				if (bindListener != null) {
629					bindListener.onBind(account);
630				}
631
632				changeStatus(Account.STATUS_ONLINE);
633			}
634		});
635		if (this.streamFeatures.hasChild("session")) {
636			Log.d(LOGTAG, account.getJid() + ": sending deprecated session");
637			IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
638			startSession.addChild("session",
639					"urn:ietf:params:xml:ns:xmpp-session");
640			this.sendUnboundIqPacket(startSession, null);
641		}
642	}
643
644	private void sendServiceDiscoveryInfo(final String server) {
645		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
646		iq.setTo(server);
647		iq.query("http://jabber.org/protocol/disco#info");
648		this.sendIqPacket(iq, new OnIqPacketReceived() {
649
650			@Override
651			public void onIqPacketReceived(Account account, IqPacket packet) {
652				List<Element> elements = packet.query().getChildren();
653				List<String> features = new ArrayList<String>();
654				for (int i = 0; i < elements.size(); ++i) {
655					if (elements.get(i).getName().equals("feature")) {
656						features.add(elements.get(i).getAttribute("var"));
657					}
658				}
659				disco.put(server, features);
660
661				if (account.getServer().equals(server)) {
662					enableAdvancedStreamFeatures();
663				}
664			}
665		});
666	}
667
668	private void enableAdvancedStreamFeatures() {
669		if (hasFeaturesCarbon()) {
670			sendEnableCarbons();
671		}
672	}
673
674	private void sendServiceDiscoveryItems(final String server) {
675		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
676		iq.setTo(server);
677		iq.query("http://jabber.org/protocol/disco#items");
678		this.sendIqPacket(iq, new OnIqPacketReceived() {
679
680			@Override
681			public void onIqPacketReceived(Account account, IqPacket packet) {
682				List<Element> elements = packet.query().getChildren();
683				for (int i = 0; i < elements.size(); ++i) {
684					if (elements.get(i).getName().equals("item")) {
685						String jid = elements.get(i).getAttribute("jid");
686						sendServiceDiscoveryInfo(jid);
687					}
688				}
689			}
690		});
691	}
692
693	private void sendEnableCarbons() {
694		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
695		iq.addChild("enable", "urn:xmpp:carbons:2");
696		this.sendIqPacket(iq, new OnIqPacketReceived() {
697
698			@Override
699			public void onIqPacketReceived(Account account, IqPacket packet) {
700				if (!packet.hasChild("error")) {
701					Log.d(LOGTAG, account.getJid()
702							+ ": successfully enabled carbons");
703				} else {
704					Log.d(LOGTAG, account.getJid()
705							+ ": error enableing carbons " + packet.toString());
706				}
707			}
708		});
709	}
710
711	private void processStreamError(Tag currentTag) {
712		Log.d(LOGTAG, "processStreamError");
713	}
714
715	private void sendStartStream() throws IOException {
716		Tag stream = Tag.start("stream:stream");
717		stream.setAttribute("from", account.getJid());
718		stream.setAttribute("to", account.getServer());
719		stream.setAttribute("version", "1.0");
720		stream.setAttribute("xml:lang", "en");
721		stream.setAttribute("xmlns", "jabber:client");
722		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
723		tagWriter.writeTag(stream);
724	}
725
726	private String nextRandomId() {
727		return new BigInteger(50, mRandom).toString(32);
728	}
729
730	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
731		if (packet.getId() == null) {
732			String id = nextRandomId();
733			packet.setAttribute("id", id);
734		}
735		packet.setFrom(account.getFullJid());
736		this.sendPacket(packet, callback);
737	}
738
739	public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
740		if (packet.getId() == null) {
741			String id = nextRandomId();
742			packet.setAttribute("id", id);
743		}
744		this.sendPacket(packet, callback);
745	}
746
747	public void sendMessagePacket(MessagePacket packet) {
748		this.sendPacket(packet, null);
749	}
750
751	public void sendPresencePacket(PresencePacket packet) {
752		this.sendPacket(packet, null);
753	}
754	
755	private synchronized void sendPacket(final AbstractStanza packet,
756			PacketReceived callback) {
757		// TODO dont increment stanza count if packet = request packet or ack;
758		++stanzasSent;
759		tagWriter.writeStanzaAsync(packet);
760		if (callback != null) {
761			if (packet.getId() == null) {
762				packet.setId(nextRandomId());
763			}
764			packetCallbacks.put(packet.getId(), callback);
765		}
766	}
767
768	public void sendPing() {
769		if (streamFeatures.hasChild("sm")) {
770			tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
771		} else {
772			IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
773			iq.setFrom(account.getFullJid());
774			iq.addChild("ping", "urn:xmpp:ping");
775			this.sendIqPacket(iq, null);
776		}
777	}
778
779	public void setOnMessagePacketReceivedListener(
780			OnMessagePacketReceived listener) {
781		this.messageListener = listener;
782	}
783
784	public void setOnUnregisteredIqPacketReceivedListener(
785			OnIqPacketReceived listener) {
786		this.unregisteredIqListener = listener;
787	}
788
789	public void setOnPresencePacketReceivedListener(
790			OnPresencePacketReceived listener) {
791		this.presenceListener = listener;
792	}
793
794	public void setOnJinglePacketReceivedListener(
795			OnJinglePacketReceived listener) {
796		this.jingleListener = listener;
797	}
798
799	public void setOnStatusChangedListener(OnStatusChanged listener) {
800		this.statusListener = listener;
801	}
802
803	public void setOnBindListener(OnBindListener listener) {
804		this.bindListener = listener;
805	}
806
807	public void disconnect(boolean force) {
808		changeStatus(Account.STATUS_OFFLINE);
809		Log.d(LOGTAG, "disconnecting");
810		try {
811			if (force) {
812				socket.close();
813				return;
814			}
815			new Thread(new Runnable() {
816
817				@Override
818				public void run() {
819					if (tagWriter.isActive()) {
820						tagWriter.finish();
821						try {
822							while (!tagWriter.finished()) {
823								Log.d(LOGTAG, "not yet finished");
824								Thread.sleep(100);
825							}
826							tagWriter.writeTag(Tag.end("stream:stream"));
827						} catch (IOException e) {
828							Log.d(LOGTAG, "io exception during disconnect");
829						} catch (InterruptedException e) {
830							Log.d(LOGTAG, "interrupted");
831						}
832					}
833				}
834			}).start();
835		} catch (IOException e) {
836			Log.d(LOGTAG, "io exception during disconnect");
837		}
838	}
839
840	public boolean hasFeatureRosterManagment() {
841		if (this.streamFeatures == null) {
842			return false;
843		} else {
844			return this.streamFeatures.hasChild("ver");
845		}
846	}
847
848	public boolean hasFeatureStreamManagment() {
849		if (this.streamFeatures == null) {
850			return false;
851		} else {
852			return this.streamFeatures.hasChild("sm");
853		}
854	}
855
856	public boolean hasFeaturesCarbon() {
857		return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
858	}
859
860	public boolean hasDiscoFeature(String server, String feature) {
861		if (!disco.containsKey(server)) {
862			return false;
863		}
864		return disco.get(server).contains(feature);
865	}
866
867	public List<String> findDiscoItemsByFeature(String feature) {
868		List<String> items = new ArrayList<String>();
869		for (Entry<String, List<String>> cursor : disco.entrySet()) {
870			if (cursor.getValue().contains(feature)) {
871				items.add(cursor.getKey());
872			}
873		}
874		return items;
875	}
876	
877	public String findDiscoItemByFeature(String feature) {
878		List<String> items = findDiscoItemsByFeature(feature);
879		if (items.size()>=1) {
880			return items.get(0);
881		}
882		return null;
883	}
884
885	public void r() {
886		this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
887	}
888
889	public int getReceivedStanzas() {
890		return this.stanzasReceived;
891	}
892
893	public int getSentStanzas() {
894		return this.stanzasSent;
895	}
896
897	public String getMucServer() {
898		return findDiscoItemByFeature("http://jabber.org/protocol/muc");
899	}
900
901	public int getTimeToNextAttempt() {
902		int interval = (int) (25 * Math.pow(1.5, attempt));
903		int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
904		return interval - secondsSinceLast;
905	}
906
907	public int getAttempt() {
908		return this.attempt;
909	}
910}