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