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