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