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