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