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