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