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