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