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.sendIqPacket(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"); //setContent("")
648			startSession.setId(nextRandomId());
649			this.sendPacket(startSession, null);
650		}
651	}
652
653	private void sendServiceDiscoveryInfo(final String server) {
654		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
655		iq.setTo(server);
656		iq.query("http://jabber.org/protocol/disco#info");
657		this.sendIqPacket(iq, new OnIqPacketReceived() {
658
659			@Override
660			public void onIqPacketReceived(Account account, IqPacket packet) {
661				List<Element> elements = packet.query().getChildren();
662				List<String> features = new ArrayList<String>();
663				for (int i = 0; i < elements.size(); ++i) {
664					if (elements.get(i).getName().equals("feature")) {
665						features.add(elements.get(i).getAttribute(
666								"var"));
667					}
668				}
669				disco.put(server, features);
670				
671				if (account.getServer().equals(server)) {
672					enableAdvancedStreamFeatures();
673				}
674			}
675		});
676	}
677	
678	private void enableAdvancedStreamFeatures() {
679		if (hasFeaturesCarbon()) {
680			sendEnableCarbons();
681		}
682	}
683	
684	private void sendServiceDiscoveryItems(final String server) {
685		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
686		iq.setTo(server);
687		iq.query("http://jabber.org/protocol/disco#items");
688		this.sendIqPacket(iq, new OnIqPacketReceived() {
689
690			@Override
691			public void onIqPacketReceived(Account account, IqPacket packet) {
692				List<Element> elements = packet.query().getChildren();
693				for (int i = 0; i < elements.size(); ++i) {
694					if (elements.get(i).getName().equals("item")) {
695						String jid = elements.get(i).getAttribute(
696								"jid");
697						sendServiceDiscoveryInfo(jid);
698					}
699				}
700			}
701		});
702	}
703
704	private void sendEnableCarbons() {
705		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
706		iq.addChild("enable","urn:xmpp:carbons:2");
707		this.sendIqPacket(iq, new OnIqPacketReceived() {
708
709			@Override
710			public void onIqPacketReceived(Account account, IqPacket packet) {
711				if (!packet.hasChild("error")) {
712					Log.d(LOGTAG, account.getJid()
713							+ ": successfully enabled carbons");
714				} else {
715					Log.d(LOGTAG, account.getJid()
716							+ ": error enableing carbons " + packet.toString());
717				}
718			}
719		});
720	}
721
722	private void processStreamError(Tag currentTag) {
723		Log.d(LOGTAG, "processStreamError");
724	}
725
726	private void sendStartStream() throws IOException {
727		Tag stream = Tag.start("stream:stream");
728		stream.setAttribute("from", account.getJid());
729		stream.setAttribute("to", account.getServer());
730		stream.setAttribute("version", "1.0");
731		stream.setAttribute("xml:lang", "en");
732		stream.setAttribute("xmlns", "jabber:client");
733		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
734		tagWriter.writeTag(stream);
735	}
736
737	private String nextRandomId() {
738		return new BigInteger(50, random).toString(32);
739	}
740
741	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
742		if (packet.getId()==null) {
743			String id = nextRandomId();
744			packet.setAttribute("id", id);
745		}
746		packet.setFrom(account.getFullJid());
747		this.sendPacket(packet, callback);
748	}
749
750	public void sendMessagePacket(MessagePacket packet) {
751		this.sendPacket(packet, null);
752	}
753
754	public void sendMessagePacket(MessagePacket packet,
755			OnMessagePacketReceived callback) {
756		this.sendPacket(packet, callback);
757	}
758
759	public void sendPresencePacket(PresencePacket packet) {
760		this.sendPacket(packet, null);
761	}
762
763	public void sendPresencePacket(PresencePacket packet,
764			OnPresencePacketReceived callback) {
765		this.sendPacket(packet, callback);
766	}
767	
768	private synchronized void sendPacket(final AbstractStanza packet, PacketReceived callback) {
769		// TODO dont increment stanza count if packet = request packet or ack;
770		++stanzasSent;
771		tagWriter.writeStanzaAsync(packet);
772		if (callback != null) {
773			if (packet.getId()==null) {
774				packet.setId(nextRandomId());
775			}
776			packetCallbacks.put(packet.getId(), callback);
777		}
778	}
779	
780	public void sendPing() {
781		if (streamFeatures.hasChild("sm")) {
782			tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
783		} else {
784			IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
785			iq.setFrom(account.getFullJid());
786			iq.addChild("ping","urn:xmpp:ping");
787			this.sendIqPacket(iq, null);
788		}
789	}
790
791	public void setOnMessagePacketReceivedListener(
792			OnMessagePacketReceived listener) {
793		this.messageListener = listener;
794	}
795
796	public void setOnUnregisteredIqPacketReceivedListener(
797			OnIqPacketReceived listener) {
798		this.unregisteredIqListener = listener;
799	}
800
801	public void setOnPresencePacketReceivedListener(
802			OnPresencePacketReceived listener) {
803		this.presenceListener = listener;
804	}
805	
806	public void setOnJinglePacketReceivedListener(OnJinglePacketReceived listener) {
807		this.jingleListener = listener;
808	}
809
810	public void setOnStatusChangedListener(OnStatusChanged listener) {
811		this.statusListener = listener;
812	}
813	
814	public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) {
815		this.tlsListener = listener;
816	}
817	
818	public void setOnBindListener(OnBindListener listener) {
819		this.bindListener = listener;
820	}
821
822	public void disconnect(boolean force) {
823		changeStatus(Account.STATUS_OFFLINE);
824		Log.d(LOGTAG,"disconnecting");
825		try {
826		if (force) {
827				socket.close();
828				return;
829		}
830		if (tagWriter.isActive()) {
831			tagWriter.finish();
832			while(!tagWriter.finished()) {
833				//Log.d(LOGTAG,"not yet finished");
834				Thread.sleep(100);
835			}
836			tagWriter.writeTag(Tag.end("stream:stream"));
837		}
838		} catch (IOException e) {
839			Log.d(LOGTAG,"io exception during disconnect");
840		} catch (InterruptedException e) {
841			Log.d(LOGTAG,"interupted while waiting for disconnect");
842		}
843	}
844	
845	public boolean hasFeatureRosterManagment() {
846		if (this.streamFeatures==null) {
847			return false;
848		} else {
849			return this.streamFeatures.hasChild("ver");
850		}
851	}
852	
853	public boolean hasFeatureStreamManagment() {
854		if (this.streamFeatures==null) {
855			return false;
856		} else {
857			return this.streamFeatures.hasChild("sm");
858		}
859	}
860	
861	public boolean hasFeaturesCarbon() {
862		return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
863	}
864	
865	public boolean hasDiscoFeature(String server, String feature) {
866		if (!disco.containsKey(server)) {
867			return false;
868		}
869		return disco.get(server).contains(feature);
870	}
871	
872	public String findDiscoItemByFeature(String feature) {
873		Iterator<Entry<String, List<String>>> it = this.disco.entrySet().iterator();
874	    while (it.hasNext()) {
875	    	Entry<String, List<String>> pairs = it.next();
876	        if (pairs.getValue().contains(feature)) {
877	        	return pairs.getKey();
878	        }
879	        it.remove();
880	    }
881		return null;
882	}
883
884	public void r() {
885		this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
886	}
887
888	public int getReceivedStanzas() {
889		return this.stanzasReceived;
890	}
891	
892	public int getSentStanzas() {
893		return this.stanzasSent;
894	}
895
896	public String getMucServer() {
897		return findDiscoItemByFeature("http://jabber.org/protocol/muc");
898	}
899	
900	public boolean hasPendingSubscription(String jid) {
901		return this.pendingSubscriptions.contains(jid);
902	}
903	
904	public void addPendingSubscription(String jid) {
905		Log.d(LOGTAG,"adding "+jid+" to pending subscriptions");
906		this.pendingSubscriptions.add(jid);
907	}
908
909	public int getTimeToNextAttempt() {
910		int interval = (int) (25 * Math.pow(1.5,attempt));
911		int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
912		return interval - secondsSinceLast;
913	}
914	
915	public int getAttempt() {
916		return this.attempt;
917	}
918}