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