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