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