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