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