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