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