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