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