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