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				String resource = packet.findChild("bind").findChild("jid")
613						.getContent().split("/")[1];
614				account.setResource(resource);
615				if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
616					smVersion = 3;
617					EnablePacket enable = new EnablePacket(smVersion);
618					tagWriter.writeStanzaAsync(enable);
619				} else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
620					smVersion = 2;
621					EnablePacket enable = new EnablePacket(smVersion);
622					tagWriter.writeStanzaAsync(enable);
623				}
624				sendServiceDiscoveryInfo(account.getServer());
625				sendServiceDiscoveryItems(account.getServer());
626				if (bindListener != null) {
627					bindListener.onBind(account);
628				}
629
630				changeStatus(Account.STATUS_ONLINE);
631			}
632		});
633		if (this.streamFeatures.hasChild("session")) {
634			Log.d(LOGTAG, account.getJid() + ": sending deprecated session");
635			IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
636			startSession.addChild("session",
637					"urn:ietf:params:xml:ns:xmpp-session");
638			this.sendUnboundIqPacket(startSession, null);
639		}
640	}
641
642	private void sendServiceDiscoveryInfo(final String server) {
643		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
644		iq.setTo(server);
645		iq.query("http://jabber.org/protocol/disco#info");
646		this.sendIqPacket(iq, new OnIqPacketReceived() {
647
648			@Override
649			public void onIqPacketReceived(Account account, IqPacket packet) {
650				List<Element> elements = packet.query().getChildren();
651				List<String> features = new ArrayList<String>();
652				for (int i = 0; i < elements.size(); ++i) {
653					if (elements.get(i).getName().equals("feature")) {
654						features.add(elements.get(i).getAttribute("var"));
655					}
656				}
657				disco.put(server, features);
658
659				if (account.getServer().equals(server)) {
660					enableAdvancedStreamFeatures();
661				}
662			}
663		});
664	}
665
666	private void enableAdvancedStreamFeatures() {
667		if (hasFeaturesCarbon()) {
668			sendEnableCarbons();
669		}
670	}
671
672	private void sendServiceDiscoveryItems(final String server) {
673		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
674		iq.setTo(server);
675		iq.query("http://jabber.org/protocol/disco#items");
676		this.sendIqPacket(iq, new OnIqPacketReceived() {
677
678			@Override
679			public void onIqPacketReceived(Account account, IqPacket packet) {
680				List<Element> elements = packet.query().getChildren();
681				for (int i = 0; i < elements.size(); ++i) {
682					if (elements.get(i).getName().equals("item")) {
683						String jid = elements.get(i).getAttribute("jid");
684						sendServiceDiscoveryInfo(jid);
685					}
686				}
687			}
688		});
689	}
690
691	private void sendEnableCarbons() {
692		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
693		iq.addChild("enable", "urn:xmpp:carbons:2");
694		this.sendIqPacket(iq, new OnIqPacketReceived() {
695
696			@Override
697			public void onIqPacketReceived(Account account, IqPacket packet) {
698				if (!packet.hasChild("error")) {
699					Log.d(LOGTAG, account.getJid()
700							+ ": successfully enabled carbons");
701				} else {
702					Log.d(LOGTAG, account.getJid()
703							+ ": error enableing carbons " + packet.toString());
704				}
705			}
706		});
707	}
708
709	private void processStreamError(Tag currentTag) {
710		Log.d(LOGTAG, "processStreamError");
711	}
712
713	private void sendStartStream() throws IOException {
714		Tag stream = Tag.start("stream:stream");
715		stream.setAttribute("from", account.getJid());
716		stream.setAttribute("to", account.getServer());
717		stream.setAttribute("version", "1.0");
718		stream.setAttribute("xml:lang", "en");
719		stream.setAttribute("xmlns", "jabber:client");
720		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
721		tagWriter.writeTag(stream);
722	}
723
724	private String nextRandomId() {
725		return new BigInteger(50, mRandom).toString(32);
726	}
727
728	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
729		if (packet.getId() == null) {
730			String id = nextRandomId();
731			packet.setAttribute("id", id);
732		}
733		packet.setFrom(account.getFullJid());
734		this.sendPacket(packet, callback);
735	}
736
737	public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
738		if (packet.getId() == null) {
739			String id = nextRandomId();
740			packet.setAttribute("id", id);
741		}
742		this.sendPacket(packet, callback);
743	}
744
745	public void sendMessagePacket(MessagePacket packet) {
746		this.sendPacket(packet, null);
747	}
748
749	public void sendPresencePacket(PresencePacket packet) {
750		this.sendPacket(packet, null);
751	}
752	
753	private synchronized void sendPacket(final AbstractStanza packet,
754			PacketReceived callback) {
755		// TODO dont increment stanza count if packet = request packet or ack;
756		++stanzasSent;
757		tagWriter.writeStanzaAsync(packet);
758		if (callback != null) {
759			if (packet.getId() == null) {
760				packet.setId(nextRandomId());
761			}
762			packetCallbacks.put(packet.getId(), callback);
763		}
764	}
765
766	public void sendPing() {
767		if (streamFeatures.hasChild("sm")) {
768			tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
769		} else {
770			IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
771			iq.setFrom(account.getFullJid());
772			iq.addChild("ping", "urn:xmpp:ping");
773			this.sendIqPacket(iq, null);
774		}
775	}
776
777	public void setOnMessagePacketReceivedListener(
778			OnMessagePacketReceived listener) {
779		this.messageListener = listener;
780	}
781
782	public void setOnUnregisteredIqPacketReceivedListener(
783			OnIqPacketReceived listener) {
784		this.unregisteredIqListener = listener;
785	}
786
787	public void setOnPresencePacketReceivedListener(
788			OnPresencePacketReceived listener) {
789		this.presenceListener = listener;
790	}
791
792	public void setOnJinglePacketReceivedListener(
793			OnJinglePacketReceived listener) {
794		this.jingleListener = listener;
795	}
796
797	public void setOnStatusChangedListener(OnStatusChanged listener) {
798		this.statusListener = listener;
799	}
800
801	public void setOnBindListener(OnBindListener listener) {
802		this.bindListener = listener;
803	}
804
805	public void disconnect(boolean force) {
806		changeStatus(Account.STATUS_OFFLINE);
807		Log.d(LOGTAG, "disconnecting");
808		try {
809			if (force) {
810				socket.close();
811				return;
812			}
813			new Thread(new Runnable() {
814
815				@Override
816				public void run() {
817					if (tagWriter.isActive()) {
818						tagWriter.finish();
819						try {
820							while (!tagWriter.finished()) {
821								Log.d(LOGTAG, "not yet finished");
822								Thread.sleep(100);
823							}
824							tagWriter.writeTag(Tag.end("stream:stream"));
825						} catch (IOException e) {
826							Log.d(LOGTAG, "io exception during disconnect");
827						} catch (InterruptedException e) {
828							Log.d(LOGTAG, "interrupted");
829						}
830					}
831				}
832			}).start();
833		} catch (IOException e) {
834			Log.d(LOGTAG, "io exception during disconnect");
835		}
836	}
837
838	public boolean hasFeatureRosterManagment() {
839		if (this.streamFeatures == null) {
840			return false;
841		} else {
842			return this.streamFeatures.hasChild("ver");
843		}
844	}
845
846	public boolean hasFeatureStreamManagment() {
847		if (this.streamFeatures == null) {
848			return false;
849		} else {
850			return this.streamFeatures.hasChild("sm");
851		}
852	}
853
854	public boolean hasFeaturesCarbon() {
855		return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
856	}
857
858	public boolean hasDiscoFeature(String server, String feature) {
859		if (!disco.containsKey(server)) {
860			return false;
861		}
862		return disco.get(server).contains(feature);
863	}
864
865	public List<String> findDiscoItemsByFeature(String feature) {
866		List<String> items = new ArrayList<String>();
867		for (Entry<String, List<String>> cursor : disco.entrySet()) {
868			if (cursor.getValue().contains(feature)) {
869				items.add(cursor.getKey());
870			}
871		}
872		return items;
873	}
874	
875	public String findDiscoItemByFeature(String feature) {
876		List<String> items = findDiscoItemsByFeature(feature);
877		if (items.size()>=1) {
878			return items.get(0);
879		}
880		return null;
881	}
882
883	public void r() {
884		this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
885	}
886
887	public int getReceivedStanzas() {
888		return this.stanzasReceived;
889	}
890
891	public int getSentStanzas() {
892		return this.stanzasSent;
893	}
894
895	public String getMucServer() {
896		return findDiscoItemByFeature("http://jabber.org/protocol/muc");
897	}
898
899	public int getTimeToNextAttempt() {
900		int interval = (int) (25 * Math.pow(1.5, attempt));
901		int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
902		return interval - secondsSinceLast;
903	}
904
905	public int getAttempt() {
906		return this.attempt;
907	}
908}