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